Build Draftroom
KINDAreplaces $99/mosaves $1,188/yrback to the verdict
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.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the application and the migrations.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - 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 - accountfree 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 - 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 - 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.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_URLsecret | required | postgres://user:redacted@localhost:5432/draftroom | Every table carries workspace_id, including the joinable ones. |
AUTH_URL | required | https://project.supabase.co | Hosted authentication for team members only. Clients get links. |
AUTH_SERVICE_KEYsecret | required | redacted | Server side only. It must never reach the browser bundle. |
STORAGE_BUCKET | required | draftroom-files | Private. Keys are unguessable and one per version. |
REVIEW_LINK_DEFAULT_DAYS | required | 14 | A link with no expiry is a leak with a delay. |
REVIEW_WRITE_RATE_PER_HOUR | required | 60 | Per token and per address on the public review route. |
UPLOAD_MAX_BYTES | required | 524288000 | Enforced in the storage policy, not only in the browser. |
VERSION_RETENTION_DAYS | required | 1095 | How long superseded versions are kept. Approvals point at them. |
The build, in order
Tenancy you cannot forget
Every query is workspace-scoped by construction, and a suite proves it on every route.
Derived from the session, applied by construction.
Files
app/db/scope.server.tsprisma/schema.prismaA hand-written list goes stale the first busy week.
Files
tests/cross-tenant.test.tsForbidden confirms the thing exists, which is itself a leak.
Files
app/lib/errors.server.ts
done when · tick each as it passeswatch 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.
Work that moves through a stated flow
A task cannot reach delivered without passing the states in between, and every move is recorded.
The server decides, whatever the board allows you to drag.
Files
app/lib/workflow.server.tstests/transitions.test.tsThe timeline is what answers 'who moved this and when'.
Files
app/lib/activity.server.tsAn editable audit trail is decoration.
Files
prisma/schema.prisma
done when · tick each as it passeswatch 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.
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.
The approved bytes have to stay retrievable.
Files
app/lib/storage.server.tstests/versions.test.tsApplication code forgets under concurrency.
Files
prisma/migrations/An approval of an asset is an approval of nothing in particular.
Files
app/lib/approvals.server.ts
done when · tick each as it passeswatch 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.
A review link that shows one thing
A client opening a link sees the files it names and nothing else, ever.
Hidden in the template means present in the response.
Files
app/lib/review-serialiser.server.tstests/review-leak.test.tsThe scope is data, not an assumption about the route.
Files
app/lib/review-links.server.tsA link that lives forever is a link that leaks eventually.
Files
app/routes/review.$token.tsx
done when · tick each as it passeswatch 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.
Leaving, and being able toproduct builder
A client's whole history can be exported, a workspace can be restored intact, and nothing disappears quietly.
An agency that cannot leave will not join.
Files
app/lib/export.server.tstests/export-roundtrip.test.tsEither alone restores something useless.
Files
ops/backup.shtests/restore.test.tsAnd delete the objects, not just the rows.
Files
app/lib/archive.server.ts
done when · tick each as it passeswatch out- Backing up the database on a schedule and leaving object storage to its own devices produces a restore in which every approval, comment and version row is intact and every file is a broken link, which is the same as having lost the work. Back up both together, and prove the restore by opening an approved file from the restored pair.
That is the whole plan for Draftroom. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Any platform administrator or impersonation.
- Frame-accurate video annotation.
- Billing, time tracking and resource planning.
- Single sign-on and white labelling.
- Production-hardened real-time and notification behavior across many simultaneous users
- Years of edge-case handling around client permissions, review links, files, revisions, and approvals
- Polished mobile review and collaboration behavior that non-technical clients can use without training
- Operational history, project context, and team habits already accumulated inside an existing Draftroom workspace
- Enterprise features such as SSO, advanced reporting, white-labeling, onboarding, and support
- A content calendar over the same tasks, which is a view rather than a new model
- Client-visible comment threads with email replies, once the allow-list serialiser has held for a month
- Per-client branding on the review page, drawn from data rather than from a separate template
Need the files? The project pack on the verdict page hands your agent the whole brief · more project management.