Introduction
This is a public engineering log. Every meaningful technical, product, and brand decision Arqo has made — written down, in plain language, where writers, the WGA, press, and prospective collaborators can read it.
We publish this for one reason: a principle you keep internal is just a preference. A principle you say out loud is a constraint on your own behavior. We would rather be held to a high standard in public than make our standards private.
The four vows (signed April 26, 2026)
- We will never write the writer's work for them.
- We will never sell the writer's work to anyone.
- We will never tell a studio our tool replaces a writer.
- We will say what we believe in public and live with the cost.
This page is vow four in practice.
01 · Stack
Every technology choice and why we made it. An engineer reading this should be able to verify the rationale against the codebase.
Application framework
- Next.js 16with the App Router. Server Components by default, Server Actions over REST for mutations. Tradeoff: tighter coupling to Vercel's runtime than we'd like; we keep the surface area small enough to migrate if needed.
- React 19 with strict mode in development.
- TypeScript in strict mode. No implicit
anysurvives review.
Database and auth
- Supabase (Postgres + Auth + Row Level Security). Chosen over Firebase for: real Postgres (joins, transactions, migrations we can read), open-source, and RLS as the authorization model. Authorization lives in the database, not in application code that any future engineer might forget to add.
- Atomic Postgres functions on save paths (e.g.
save_scene_elements,accept_invite) so a half-applied write cannot corrupt a script.
Real-time collaboration
- Liveblocks for presence and threads. We chose hosted over self-hosted y-websocket for the speed-to-launch tradeoff. Our thread-sync glue layer (
lib/threads-sync/) keeps Liveblocks threads mirrored to Postgres so a writer can read their notes even if Liveblocks is down. Migration path off Liveblocks is documented and not currently planned.
- Resend for transactional email. Chosen over SES for: better DKIM defaults, faster warmup curve, a dashboard a non-engineer can read. DMARC is currently
p=noneduring warm-up; we will tighten top=quarantinethenp=reject.
Native shells
- iOS — Native SwiftUI (
Arqo/) with Editor v3 and a WKWebView bridge for shared surfaces. Replaced the legacy Capacitor shell (ARQ-815). - macOS — Native SwiftUI (
ArqoMac/) with a fully native editor. - Android — Capacitor wraps the production web app at
app.tryarqo.com. - Tauri desktop (beta) — wraps the web TipTap editor on macOS and Windows. Auto-updater endpoints are signed and pinned.
- Platform parity is tracked in
docs/parity-backlog.md.
Hosting and observability
- Vercel for hosting. Edge middleware, deferred routes, image optimization.
- Sentry for error and performance tracing. Configured to no-op until a DSN is set, so a fork or fresh checkout never accidentally sends events to our project.
- Upstash Redis for the per-IP AI rate-limit cache and a few other counters.
Testing
- Vitest for unit tests. Roughly 1,889 test cases as of this writing.
- Playwright for end-to-end. Separate configs for golden path, demo smoke, invite happy path.
- k6 for load tests. Four scenarios in
tests-loadtest/scenarios/: smoke, conversion funnel, autosave storm, launch spike. fdx-conformance/is its own harness for Final Draft import and export against a corpus of real scripts.
Lint and type guards
- ESLint pinned to
^9.x(the react plugin's peer dep caps it). We do not bump until the plugin updates. - A custom rule blocks
'use server'files from exporting non-async values. That bug class hit production four times before we wrote the rule. - Engine-package import boundaries:
lib/ai,lib/exporters,lib/screenplay,lib/merge,lib/schedule,lib/arq,lib/crypto, andlib/collabcannot import the Supabase client directly. Bridges live inlib/api-guards/. This keeps engine packages pure so they can ship as standalone libraries.
02 · Architecture
Server first
Mutations are Server Actions, not REST endpoints. The script editor reads from Server Components on first paint, then takes over with a small client bundle for typing. The reason is conservative: fewer API surfaces means fewer places to forget an authorization check.
Authorization
RLS is the primary defense. Every script-scoped table (scripts, scenes, script_elements, beats, characters, threads) has an RLS policy that requires either ownership or an active collaborator row.
Defense-in-depth ownership checks live at the action layer too. INVITE-008 added explicit assertOwner(scriptId) calls inside Server Actions that mutate scripts, on top of the RLS policy. The RLS catches mistakes; the action-level check catches the cases where a future engineer accidentally bypasses RLS via a service-role client.
Owner is owner forever. A Postgres trigger (scripts_guard_owner_change) refuses any UPDATE that changes the owner_id column. The only way to transfer a script is to delete and re-create it.
Persistence
ScriptStore is an interface. Both the demo workspace (LocalStorage) and the real workspace (Supabase) implement it. The editor talks to ScriptStore, not to either backend. This came out of the DEMO-003 series and is the reason demo and real-app behavior stay aligned.
Save paths are atomic Postgres functions. save_scene_elementsaccepts a scene ID and an ordered array of elements, validates owner/collaborator status, and replaces the scene's elements in a single transaction. A network blip mid-save cannot leave a scene half-written.
Editor
Per-element textareas. Each element (action, dialogue, parenthetical) is its own input. Container-level copy/paste handlers reconstruct multi-element clipboard payloads on paste. Multi-line paste does not split mid-element today; the cursor lands after the current element. That gap is tracked.
Real-time
One Liveblocks room per script. Presence (cursors, names) and threads (in-script comments) ride on the room. The lib/threads-sync/ layer mirrors thread events into Postgres so the comment history is durable independent of Liveblocks.
Demo lifecycle
The demo workspace is gated behind the NEXT_PUBLIC_DEMO_ENABLED env flag. Demo data lives in LocalStorage, scoped per-tab, and resets on login — intentional, the demo is a sandbox, not a migration source. The DEMO-006 sunset deletes the demo workspace on the August 17 launch day.
ESLint guard
'use server' files cannot export non-async values. A regression of this caused four production incidents before we wrote the rule. The rule lives in eslint-rules/.
03 · Data model
Tables
scripts— owner-only writes after ARQ-186. Read access is owner OR active collaborator. The owner column is immutable (scripts_guard_owner_changetrigger).scenes— belongs to a script. RLS inherits.script_elements— the per-element rows the editor writes throughsave_scene_elements.beats— story beats attached to a script or scene.characters— character cards. Came from a real user need: writers wanted to enter a character before they had typed a single cue. We did not add this table from a “data normalization” instinct.collaborators— owner and collaborator only. There is no editor / reader / producer tier at launch (see Brand decisions). Constrained at the database level after ARQ-181.script_invites— the invite-link table.accept_inviteis an atomic Postgres function that validates the token, creates the collaborator row, and marks the invite consumed in one transaction.threads— in-script comments. Mirrored from Liveblocks.share_tokens— read-only public links. Independent table so the access path is auditable.
Migration audit
The full migration history and current state is at docs/migration-state-audit.md. It lists every migration that has shipped and flags any that need follow-up.
Why the shape looks like it does
The data model follows the writer's mental model first, the database normaliser second. A script is the unit. Scenes belong to scripts. Elements belong to scenes. A character belongs to the script before any cue references them. None of this is an accident.
04 · Brand decisions (locked)
These are not aspirations. They are signed commitments that route every product decision.
The Iron Man suit
Arqo is the suit. The writer is Tony. The suit does not fly without the pilot. It does not write the script. It does not have its own agenda. It amplifies what the writer brings — memory across a 120-page draft, presence across the airport and the late-night phone, structure that holds while the writer works through the messy middle.
We are explicitly not building Sonny from I, Robot. We are not building the studio's writer-replacement.
The three product principles
- Arqo's AI never generates content the writer didn't ask for, in voices the writer didn't write. The “generate scene” button is the line we will not cross. Crossing it would make us a replacement.
- Every AI action is an amplifier, not an originator. Restructure, surface, suggest variations of the writer's phrasing. Never substitute.
- The writer can disable, audit, or undo every AI suggestion. Full transparency. Always exportable. Always deletable.
The no-list
- No blank-page scene generation
- No “write a script from a logline”
- No auto-coverage or auto-notes that bypass the writer
- No features designed as replacements dressed as productivity tools
- Nothing sold to studios as “reduce writer headcount”
- No monetization of user-written work — no training data sales, no resale, no syndication, ever
At launch: owner and collaborator. That is all.
There are no seats. No team tiers. No editor / reader / producer roles. A script has one owner and a small set of collaborators. The owner can never be changed. We chose this because the alternative — a permission matrix — is the front door to becoming a studio compliance tool, and that is not the product.
AI memory, not AI learning
Arqo never “learns the writer's voice.” The framing matters. The AI has memory of what the writer wrote — characters, speech patterns, structure — and uses that memory to keep the work consistent. It does not train on the writer's work. It does not generalise the writer's voice into a model that could be applied to other people's writing. The writer owns the work. We hold a working copy.
05 · Security review log
A running record of what we have hardened and what is still outstanding. Public, because vow four.
Shipped
- RLS recursion fix. The first iteration of our RLS policies hit infinite recursion when a policy needed to read a row in the same table to evaluate. The v2 fix uses
SET row_security = offinside helper functions that are themselves SECURITY DEFINER, breaking the cycle. The helpers are small and individually reviewed. - INVITE-008 ownership defense-in-depth. Every script-mutating Server Action now does an explicit
assertOwnercall on top of the RLS policy. - ARQ-186 owner-only UPDATE policy. Scripts can only be updated by their owner. Collaborators write through scoped tables (
scenes,script_elements). - Owner-immutable trigger.
scripts_guard_owner_changerefuses any UPDATE that changesowner_id. There is no path to take over someone else's script. - Per-IP AI rate limit. 30 requests per minute per IP. Tuned so the worst-case per-IP cost is bounded at roughly $0.15 per minute per attacker.
- Sentry and bounce-webhook signature verification. Resend bounce webhook payloads are signature-verified before any state mutation.
'use server'regression guard. A custom ESLint rule prevents'use server'files from exporting non-async values. That bug class hit production four times.- Atomic Postgres save functions. A network failure mid-save cannot leave a scene half-written.
Outstanding (be honest)
- DMARC
p=noneduring warm-up. We will tighten top=quarantineand thenp=rejectonce Resend warm-up clears the volume thresholds. - Liveblocks free tier. 100 monthly active users. We have not upgraded yet because we have not crossed the threshold yet. Tier upgrade is a flip of a billing setting.
- No formal pentest yet. Planned post-launch. We have run internal review and the security checklist in
docs/security.md, but a third party has not held the codebase upside down. - No bug bounty yet. Will follow the pentest.
If you find something, mail security@tryarqo.com and we will reply within one business day.
06 · Test coverage
Unit (Vitest)
Roughly 1,889 unit tests across lib/, app/, components/, and hooks/ as of this writing. Anything new ships with a unit test in the same PR — that is a hard rule for the agents and for the humans.
End-to-end (Playwright)
- Golden path — sign-up, create script, write a scene, export to FDX. The smoke test that catches the catastrophic regressions.
- Demo — the demo workspace persistence path (DEMO-005).
- Invite — happy path plus three failure cases (expired token, already-consumed token, wrong recipient). INVITE-009.
RLS regression suite
A separate Vitest suite under tests/rls/ instantiates a Supabase client per role (anon, authenticated, service-role) and asserts the expected access pattern. This is how we catch policy regressions without staring at SQL.
FDX conformance
fdx-conformance/ is its own harness. It loads a corpus of real Final Draft files, round-trips them through our import and export, and diffs the output. The corpus is private; the harness is open in the Arqo repo.
Load (k6)
01-smoke-static.js— baseline pageload.02-conversion-funnel.js— landing → sign-up → first save.03-autosave-storm.js— many concurrent writers hammering the autosave endpoint.04-launch-spike.js— the launch-day burst pattern.
The pyramid is real
We do not skip unit tests in favor of a smaller E2E suite. The pyramid catches regressions cheaply and protects the slow Playwright tier from being the first line of defense.
07 · Performance and capacity
Estimated capacity for the launch window. Honest caveat first: these are stack-architecture estimates. The full launch-week load test ships in tests-loadtest/ and we will publish numbers after the run.
Estimated capacity
- ~50,000 total registered users on the current Supabase Pro tier.
- ~300–800 concurrent active editors (writers actively typing).
- ~3,000–5,000 concurrent connected users (idle dashboards, presence-only sessions).
Where the bottleneck is
The first ceiling we will hit is Liveblocks' tier limits — the free tier caps at 100 monthly active users. The tier upgrade is a billing flip, not a re-architecture.
The second ceiling is the Supabase pooler. Pro tier allocations are comfortable for our expected concurrent write load through the September 2026 launch.
Storage
A typical user holds roughly 250 KB of data — a few scripts, scenes, elements, beats. Pro tier ships 8 GB, which gives us headroom for ~32,000 typical users without tier change.
What we have not measured yet
- Real-world cold-start latency on Vercel for the editor route after a quiet hour.
- The autosave-storm scenario at 1,000 concurrent writers.
- Liveblocks at 500 concurrent rooms with 5 collaborators each.
These will be in the launch capacity report. If a number on this page goes stale, the timestamp at the top will tell you so.
08 · Build log
The pace of the launch sprint. Counts pulled from the git log, not from a tracker.
Day 1 — April 28, 2026
48 commits to main. Headline: the INVITE epic landed end-to-end (server, client, email template, RLS policies, defense-in-depth ownership checks). The four-failure-case Playwright suite shipped same day.
Day 2 — April 29, 2026
89 commits to main. Headline: DEMO-SUNSET work — the demo workspace gating, the September 2026 deletion path, and the Playwright smoke for demo persistence. Several score of fixes around the dashboard and the editor copy/paste path.
Day 3 — April 30, 2026
42 commits to main so far. Headline: defense-in-depth ownership checks on script actions (INVITE-008), public collab tooltip on first run (INVITE-010), public engineering log (this page).
Across the sprint
~179 commits, ~70 PRs merged, three days. Full PR list is on GitHub. We move fast on purpose. The ship cadence is the product.
09 · Known limits
What Arqo does not do today. Plain language, because vow four.
Import and export gaps
- No PDF import parser. Final Draft (
.fdx) and Fountain (.fountain) are the two import paths. PDF is an output, not an input. - Multi-line paste does not split mid-element. When a writer pastes a multi-line clipboard payload, the cursor lands after the current element rather than splitting through it. Tracked.
Real-time collaboration
- Liveblocks free tier caps real-time collab at ~100 monthly active users. Tier upgrade is a billing flip.
- Per-script collaborators are practically capped by Liveblocks room size. Comfortable range is 10–25 per script.
Demo workspace
- Demo edit persistence requires the writer to be online. No IndexedDB layer for demo edits. The demo is a sandbox, not a draft.
- DEMO-006 sunset deletes the demo workspace on launch day, September 2026. There is no migration path from demo to a real workspace. By design.
Out of v1 scope
- AI auto-translation
- Android stylus input (Apple Pencil margin notes ship on iPad; on-script ink is rolling out)
- Custom themes beyond the two we ship
- Third-party plugin API
- Production scheduling (lives in JMNPR Labs, not Arqo — see the manifesto for the screenwriting-only refocus)
What is not on this list, and why
We are not listing features other tools have that we have decided not to build. That list is on the manifesto under “What we will not build” — those are decisions, not gaps.
If you are looking for something here that you expected to find, write to hello@tryarqo.com. We would rather know than guess.