TreeMap
A free, open-source disk-space visualizer. Squarified treemap, duplicate hunting, disk-full forecasting, and cleanup that only ever moves files to the Trash.
Writing software that stays out of your way. Your files, your structure, your disk — no cloud required.
ProductivityTypeScript · JavaScriptGitHub @saboteur-worksOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
A local-first writing studio
Translation pending
Taken from the repository README
Loading the full README. The saved excerpt is shown below.
GetWrite is a local-first writing workspace for structured, long-form projects — novels, serials, articles, game docs, and more. There is no database and no cloud: every project, resource, and piece of metadata is a plain file or folder on your own machine, so your work stays inspectable, backup-friendly, and yours.Read the full README ↗GetWrite is a local-first writing workspace for structured, long-form projects — novels, serials, articles, game docs, and more. There is no database and no cloud: every project, resource, and piece of metadata is a plain file or folder on your own machine, so your work stays inspectable, backup-friendly, and yours.
projects/ — no server, no account, no lock-in. The filesystem is the data model.GetWrite ships as a desktop app for macOS, Windows, and Linux. Download the latest build from the Releases page.
The current builds are not yet signed by Apple/Microsoft, so the OS will warn before the first launch. See where GetWrite keeps your work.
The rest of this README is for developing GetWrite from source.
docs/features (project types, sidecars, data types)specs/002-define-data-models/project-types/getwrite-config/templates/project-types/frontend/srcNode.js >= 24 (we use Volta in CI/dev). If using nvm:
pnpm (preferred package manager). Install with npm if needed:
npm install -g pnpm
pnpm install
frontend alone):cd frontend
pnpm install
cd frontend
pnpm exec vitest
pnpm run test
cd frontend
pnpm run dev
GetWrite reads a small set of environment variables. None are required for the default local/desktop experience — the app resolves sensible defaults when they are unset.
| Variable | Purpose |
|---|---|
GETWRITE_PROJECTS_DIR | Absolute path to the single-tenant projects directory (the local/desktop store). Injected by the Electron main process; falls back to <cwd>/../projects when unset. |
GETWRITE_DATA_ROOT | Hosted multi-tenant only. Base directory under which each signed-in user's isolated data root lives at <data-root>/<userId>/. Distinct from GETWRITE_PROJECTS_DIR and has no fallback — if a request resolves to a signed-in user while this is unset, resolution fails closed rather than leaking into the shared directory. See ADR-018. |
GETWRITE_ENABLE_DEV_IDENTITY | Hosted/dev only. Opt-in flag that activates the interim development identity source, which reads a userId from the x-getwrite-dev-user request header. Inert (no identity asserted) unless set. This is a scaffold for exercising the tenant-resolution path — not production authentication — and logs a loud warning when active. |
GETWRITE_CLI_TESTING | Set to 1 to suppress process.exit when invoking getwrite-cli commands from tests. |
GETWRITE_SHARING | Set by the desktop app for its own server, not meant to be set by hand. 1 means home-network sharing is in effect and the pairing gate (frontend/proxy.ts) is on; any other value or unset leaves the gate off. Feature 75, merged but not yet released. See docs/features/home-network-sharing.md. |
GETWRITE_SHARING_DIR | Set by the desktop app for its own server, not meant to be set by hand. The directory (Electron userData) holding pairing-state.json and device-credentials.json. |
GETWRITE_BIND | Set by the desktop app for its own server, not meant to be set by hand. The address the server was told to listen on (127.0.0.1 or 0.0.0.0). If it names a non-loopback address while is not , the server refuses every request that is not the desktop window's. Unset means undeclared and changes nothing. |
GetWrite's identity layer is a hybrid: tenant content (projects, resources, revisions) always stays on the filesystem/object store, exactly as it does for local/desktop use; accounts — credentials, sessions, email verification, password reset — are handled by better-auth backed by PostgreSQL, entirely opt-in. See ADR-020 for the full rationale.
Omitting DATABASE_URL/BETTER_AUTH_SECRET keeps the instance in account-free local-first mode: no Postgres connection is attempted, no login screen is shown, and every route behaves exactly as it does today. Set both to enable hosted auth, both for Saboteur's primary hosted instance and for third-party self-hosters who want multi-user accounts.
| Variable | Purpose |
|---|---|
DATABASE_URL | Postgres connection string. Hard prerequisite for hosted auth, alongside BETTER_AUTH_SECRET — without both, isHostedAuthActive() is false and no Postgres connection is attempted. Also backs better-auth's rate-limit storage (rateLimit.storage: "database"). Note this only stores the counters — for the counters to key on a trustworthy client IP (rather than a spoofable one), you must also set AUTH_TRUSTED_PROXIES or AUTH_IP_ADDRESS_HEADERS below. |
BETTER_AUTH_SECRET | Secret better-auth uses to sign sessions/tokens. Required alongside DATABASE_URL to activate hosted auth. A plain shared environment variable, injected identically into every instance of the hosted service; rotation is a coordinated redeploy with the new value, not a runtime feature. |
BETTER_AUTH_URL | better-auth's baseURL — this deployment's own canonical URL. Falls back to http://localhost:3000 for local development, but real deployments (hosted and self-host) must set it explicitly, and to an https:// URL: better-auth derives the session cookie's Secure flag from this protocol, so an http:// value ships session cookies without Secure. Also the source of the allowed Origin/Referer for the CSRF check on state-changing tenant routes. |
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_FROM | nodemailer SMTP transport config for the email-verification and password-reset emails. Any SMTP-compatible provider works. SMTP_PORT: 465 implies implicit TLS (secure: true); other ports use STARTTLS. |
AUTH_SIGNUP_ALLOWLIST | Optional. Gates who may sign up: a comma- and/or newline-separated list of exact emails and/or @domain.com wildcards, matched case-insensitively. Unset means open signup. Saboteur's primary hosted instance sets this; self-hosters may leave it open or configure their own policy. |
AUTH_TRUSTED_PROXIES | Set one of this or AUTH_IP_ADDRESS_HEADERS on any internet-facing deployment, or login rate limiting (FR16) is bypassable by rotating the header. Comma/newline list of your proxy/load-balancer IPs or CIDR ranges; better-auth strips the forwarded chain to the first hop that isn't one of these to find the real client IP. Unset ⇒ better-auth trusts verbatim (spoofable) and logs a startup warning. |
frontend/ — Next.js frontend, components, tests, and runtime modelsgetwrite-config/ — example runtime configuration and templates shipped alongside the appspecs/ — specification artifacts and example project-type definitions used for docs and testsdocs/ — user- and developer-facing documentation (see docs/features)cli/ — getwrite-cli workspace package; bundled to cli/dist/bin/getwrite-cli.cjsKey model files live under frontend/src/lib/models/:
schemas.ts — Zod runtime schemas and helpers (use validateProjectType() / validateProjectTypeFile())project-creator.ts — scaffolding helper createProjectFromType() which validates specs and creates folders/resourcessidecar.ts — read/write helpers for sidecar metadataSee docs/features for more detailed docs on project types, sidecars, and data types.
specs/002-define-data-models/project-types/.getwrite-config/templates/project-types/.frontend/src/lib/models/schemas.ts and add unit tests under frontend/tests/unit.frontend workspace.pnpm run test target executes the CI test runner for the frontend workspace.002-define-data-models branch for data model changes.standards/ (TypeScript implementation, package selection, template integrity).validateProjectTypeFile() locally to inspect problems.docs/features/project-types.md — project-type spec guidancedocs/features/sidecars.md — sidecar metadata guidancedocs/features/data/data-types.md — data type referenceGETWRITE_SHARING1GETWRITE_WINDOW_SECRET | Set by the desktop app for its own server, not meant to be set by hand. A per-launch random secret the desktop window sends in the x-getwrite-window header so the server can tell it from other clients. |
GETWRITE_READINESS_KEY | Set by the desktop app for its own server, not meant to be set by hand. A per-launch random key, separate from the window secret, with which the server answers the desktop's start-up identity check (GET /api/sharing/ready) so the app knows the server on its port is the one it forked. Unset means that route answers 404. Feature 80, merged to main, not released. |
x-forwarded-forx-forwarded-forAUTH_IP_ADDRESS_HEADERS | Alternative to AUTH_TRUSTED_PROXIES. Comma/newline list of headers to read the client IP from instead of x-forwarded-for — e.g. a platform's dedicated, client-unforgeable client-IP header (cf-connecting-ip, fly-client-ip, true-client-ip, …). Prefer this when your platform sets such a header. |