Build Draftroom

KINDAreplaces $99/mosaves $1,188/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

An agency workflow tool whose hard problems are tenant isolation that cannot be forgotten in a new route, review links that expose exactly one scope, and file versions where the approved bytes are still the approved bytes a year later.

estimated effort multi-daythe files for this build are in the project pack

RuntimeRemix with TypeScript and TailwindDataPostgreSQL through Prisma, with workspace scope applied in one placeIdentityHosted authentication for the team; clients get scoped links, not accountsFilesObject storage with a new key per version and no guessable pathsReviewA server-side allow-list serialiser for the public review page

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the application and the migrations.

    Get it nodejs.org, or nvm install 22. open ↗

    Verify node --version prints v22 or higher

  2. installfree locally, a few pounds a month managed

    Why Tenant scope and the single-current-version rule are both database constraints, and constraints are the point.

    Get it A managed instance, or a container locally.

    Verify Migrations apply and the current-version constraint rejects a second current row

  3. free tier is ample for one agency

    Why Team login and file storage are the two pieces worth renting, and signed uploads are how files avoid your server.

    Get it Create a project, enable email authentication, and create a private storage bucket.

    Verify A team member can sign in and a signed upload lands in the bucket

  4. have readyfree

    Why The cross-tenant test is only convincing when two workspaces contain similarly named projects, which is exactly when a leak would be missed by eye.

    Get it Seed one demo agency with several clients, and a second workspace whose project names deliberately collide.

    Verify The cross-tenant suite runs against both

  5. decidefree

    Why Each is enforced in code and asserted by a test, and a review link default of 'never expires' is a decision even when nobody made it.

    Get it Write down the default review link lifetime, the write rate limit per token and per address, and how long archived work and old file versions are kept.

    Verify All three are in the configuration and named in tests

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- workspaces(id, name, created_at) and memberships(user_id, workspace_id, role) · role is OWNER or MEMBER, and membership is the only route to any data
- clients(id, workspace_id, name, archived_at) and projects(id, workspace_id, client_id, name, archived_at)
- tasks(id, workspace_id, project_id, title, status, priority, start_date, due_date, delivery_date, created_at, updated_at)
- file_assets(id, workspace_id, task_id, name) and file_versions(id, asset_id, ordinal, storage_key, bytes, sniffed_type, sha256, uploaded_by, uploaded_at, is_current)
- comments(id, workspace_id, task_id, file_version_id, author_id, body, visibility) · visibility is INTERNAL or CLIENT_VISIBLE and defaults to INTERNAL
- approvals(id, workspace_id, file_version_id, decision, decided_by, decided_via_link_id, comment, decided_at) · pinned to a version, never to an asset
- review_links(id, workspace_id, token_hash, scope_json, permissions_json, expires_at, revoked_at, created_by, last_used_at)
- Every table carries workspace_id, including the ones you could reach by a join, because the join you forget is the leak.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
DATABASE_URLsecretrequiredpostgres://user:redacted@localhost:5432/draftroomEvery table carries workspace_id, including the joinable ones.
AUTH_URLrequiredhttps://project.supabase.coHosted authentication for team members only. Clients get links.
AUTH_SERVICE_KEYsecretrequiredredactedServer side only. It must never reach the browser bundle.
STORAGE_BUCKETrequireddraftroom-filesPrivate. Keys are unguessable and one per version.
REVIEW_LINK_DEFAULT_DAYSrequired14A link with no expiry is a leak with a delay.
REVIEW_WRITE_RATE_PER_HOURrequired60Per token and per address on the public review route.
UPLOAD_MAX_BYTESrequired524288000Enforced in the storage policy, not only in the browser.
VERSION_RETENTION_DAYSrequired1095How long superseded versions are kept. Approvals point at them.

The build, in order

  1. Tenancy you cannot forget

    Every query is workspace-scoped by construction, and a suite proves it on every route.

    1. Derived from the session, applied by construction.

      Files app/db/scope.server.tsprisma/schema.prisma

    2. A hand-written list goes stale the first busy week.

      Files tests/cross-tenant.test.ts

    3. Forbidden confirms the thing exists, which is itself a leak.

      Files app/lib/errors.server.ts

    done when · tick each as it passes
    watch out
    • Scoping by adding a workspace condition to the where clause in each route works until somebody adds a route in a hurry, and then there is no error and no alarm, just another agency's campaign on a page. Put the scope somewhere every query must pass, and make the cross-tenant test enumerate routes automatically so a new one is covered the day it is written.
  2. Work that moves through a stated flow

    A task cannot reach delivered without passing the states in between, and every move is recorded.

    1. The server decides, whatever the board allows you to drag.

      Files app/lib/workflow.server.tstests/transitions.test.ts

    2. The timeline is what answers 'who moved this and when'.

      Files app/lib/activity.server.ts

    3. An editable audit trail is decoration.

      Files prisma/schema.prisma

    done when · tick each as it passes
    watch out
    • Letting the board set any status because the drag-and-drop library makes it easy means work arrives at delivered without ever having been approved, and nobody can reconstruct whether it was reviewed or simply dragged. Put the allowed transitions in a table the server checks, and record every move with who made it.
  3. Versions where the approved bytes stay approved

    Every upload is a new version, the current one is unambiguous, and an approval points at a version.

    1. The approved bytes have to stay retrievable.

      Files app/lib/storage.server.tstests/versions.test.ts

    2. Application code forgets under concurrency.

      Files prisma/migrations/

    3. An approval of an asset is an approval of nothing in particular.

      Files app/lib/approvals.server.ts

    done when · tick each as it passes
    watch out
    • Replacing a file by writing over the same storage key means yesterday's approval now refers to today's bytes, so the client approved something nobody can produce and the dispute is unwinnable. Write a new key per version, never overwrite, and pin every approval to a version id rather than to the asset.
  4. A review link that shows one thing

    A client opening a link sees the files it names and nothing else, ever.

    1. Hidden in the template means present in the response.

      Files app/lib/review-serialiser.server.tstests/review-leak.test.ts

    2. The scope is data, not an assumption about the route.

      Files app/lib/review-links.server.ts

    3. A link that lives forever is a link that leaks eventually.

      Files app/routes/review.$token.tsx

    done when · tick each as it passes
    watch out
    • Building the review page by passing the task object to the template and hiding the internal comments with a condition in the markup ships every internal comment in the response payload, where anyone can read it with the browser's developer tools; the same mistake with a nested project object exposes the client's other work. Serialise from an explicit list of fields on the server, and assert in a test that the response body contains no internal comment text.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more project management.