# Build a paid SaaS app with an AI coding agent, from real problem to checked launch
> Validate a customer job, scope and build a useful paid SaaS v1 with an AI coding agent, test tenant and payment boundaries, then approve and operate its release.
By BIGCHANGE.AI Editorial
Published: 2026-10-22T23:37:37.942Z
Updated: 2026-10-22T23:37:37.942Z
Canonical: https://bigchange.ai/blog/ai-agent-paid-saas-lifecycle-guide

AI-generated conceptual illustration by BIG CHANGE.
An AI coding agent can build a development app from a reviewed specification. The owner still has to identify a real customer job, decide the offer, inspect the implementation, observe the denied paths and authorize the public release. This guide walks through that whole task using a paid team checklist workspace as a selected reference. Use that model only if it matches your actual permitted customer problem. BIG CHANGE did not interview customers, run Agent, build the app, test a payment or deploy it.
## The big change
- **What changed:** [Replit Agent Plan Mode](https://docs.replit.com/features/agent/plan-mode) lets you review a plan before code or data changes, so the brief can cover the complete customer journey.
- **Why it matters:** The saved checklist, tenant boundary, payment decision and release can be tested as one product.
- **What to watch:** A completed Agent task, successful build or Checkout return does not establish demand, authorization, settled card payment or a safe public service.
## 1. Decide whether to build
Begin with evidence you are permitted to use: consenting customer conversations, an existing task you own, support requests or lawful requirements. Record who performs the job, what they do now, how often, what fails and which saved result would help. Compare a configurable existing tool with a new app. If the task, customer authority or useful first result is unclear, investigate before commissioning code. Never ask an agent to supply imaginary interviews, customers, market size or revenue.
Fill the charter below. Define an actor, input, action, saved result and next user. The selected app has one workspace owner who creates a recurring checklist and assigns a dated run; an invited member completes it; the owner accepts or returns it. A booking or document product needs a different contract, even if the same build and release sequence applies. Choose the actual price, currency, tax treatment, trial, cancellation, failed payment and refund policy before taking live money. Decide data rights, retention, export, support and spending authority.
## 2. Choose and plan the development route
The selected route uses React, Express, PostgreSQL, [Replit-managed Clerk](https://docs.replit.com/features/auth-and-identity/clerk-auth), [Stripe-hosted subscription Checkout](https://docs.stripe.com/billing/subscriptions/build-subscriptions.md?payment-ui=checkout&ui=stripe-hosted) and Billing Portal. Check the current Node runtime, package versions, generated auth proxy, account capabilities and deployment type. [Replit separates development and production databases](https://docs.replit.com/features/data-and-storage/development-and-production); keep development identities, Stripe test keys and synthetic records separate too. Check [Agent billing](https://docs.replit.com/billing/ai-billing), hosting, database, identity and Stripe costs against your own accounts. Preparing this specification requires no paid Agent run.
Give Agent the filled charter and the complete build brief, application contract, SQL and billing contract below. Use Plan Mode first. Require a route/file map, migration, permission matrix, provider actions, environment split, cost-sensitive steps and tests. Review the plan before permitting changes to a controlled development project. Reject a dashboard or a payment page that replaces the useful assignment and review job. Replit's [task board](https://docs.replit.com/references/agent/task-board) tracks Agent work; its Done state does not count as owner acceptance.
Implement in reviewable slices: verified enrollment and membership, template/version and assignment, member completion and owner review, scoped history/export, then hosted test billing. Inspect each diff and migration. The service must let the whole job survive refresh; a collection of disconnected forms is incomplete.
## 3. Observe the allowed and denied journeys
Use two real test identities in workspace A and a separate owner in workspace B. Complete template creation, invitation, assignment, item completion, submission, return or acceptance, then reload lists, details and history. Confirm that a new template version does not rewrite an assigned run. Send direct API requests with B's resource IDs from A. Test a member against owner routes, a wrong or expired invite, revocation, replay, removal with an existing provider session, stale versions, duplicate mutations and state races. Inspect database rows and history counts on denials; a hidden button proves little.
Configure a Stripe **test** Product and fixed monthly card Price only after deciding the offer. The server creates one mapped Customer and at most one unresolved Checkout attempt, sends the browser to hosted Checkout, verifies signed raw-body webhooks and computes paid access from current provider evidence. The success URL reports a return, not an entitlement. Test lost responses, duplicate requests, webhook order, failed renewal, period end, wrong Customer or Price, an extra subscription and non-card or zero payment. Existing scoped records and export remain available after lapse under the disclosed policy; new run creation is the paid action. [Stripe's Portal](https://docs.stripe.com/customer-management/configure-portal) manages the mapped Customer on demand.
The accepted reference pins Stripe request API `2026-09-30.endive` and checks the webhook endpoint event version separately. Confirm that the actual SDK and account expose these documented fields before coding. If they differ, stop and rederive the adapter. The strict predicate can temporarily deny new runs during payment failure. Explain that outcome in customer terms.
## 4. Approve a separate public release
Inspect the complete generated code, lockfile, migration history, auth proxy order, secrets, audience and rollback route. Configure distinct production Clerk, PostgreSQL and Stripe resources only after a separate owner decision for the offer, live payment, database change and public audience. [Replit publishing controls](https://docs.replit.com/features/publishing/overview) must support the server and webhook; a static page cannot run this contract. Repeat a small permitted and denied journey at the actual production URL. If a live transaction is not authorized, record live billing as unverified and hold that paid launch.
Assign support, access removal, failed-billing, cost, retention, backup and recovery owners. Check the host's [data recovery options](https://docs.replit.com/features/data-and-storage/data-recovery) and rehearse a restore before calling it tested. Reconcile uncertain Stripe events against their exact IDs and current provider objects. A code rollback cannot undo a migration or payment.
Related BIG CHANGE guides cover [a two-plan subscription manager](https://bigchange.ai/blog/ai-coding-agent-two-plan-subscription-manager-guide), [finishing and releasing an existing Replit app](https://bigchange.ai/blog/replit-agent-existing-app-first-release-guide), and [a restricted internal staff tool](https://bigchange.ai/blog/ai-agent-internal-staff-tool-lifecycle-guide). Those answer narrower or different jobs; this guide follows a newly scoped paid SaaS product from customer problem through operation.
The material below is a copyable **proposed** implementation, not tested application code. Keep bracketed charter answers private and replace them with actual owner evidence. If the real job differs from recurring assignment and review, redesign the entities, routes, schema and checks before implementation.
## Copyable product charter
| Decision | Owner's entry and evidence |
| --- | --- |
| Person and problem | [Specific permitted customer role], [recurring task], [current method], [observed friction and source/date]. |
| Evidence and rights | [Consent or lawful requirement], [notes location], [permission for any data or quotation]. |
| Existing alternative | [Actual tool or process], [why configuring it does not solve this job]. |
| First valuable action | [Actor], [input], [action], [saved result], [next user]. |
| Acceptance | [Permitted user] completes [whole action] with [authorized test data]; [denied actor] cannot read or change it. |
| Customer boundary | [Workspace and roles], [invitation], [retention and export], [access removal owner]. |
| v1 exclusions | [Deferred features] and [requirements that would make this version unsuitable]. |
| Commercial choice | [Real price, currency, tax, terms, trial, refund and cancellation policy], [live-mode approver]. |
| Resource and release owner | [Repository, identity, database, Stripe, host], [budget], [release and support owners]. |
Hold the build if the first valuable action, lawful data use or customer authority is unresolved. Hold live payment and public release until the offer and operations decisions are made.
## Selected application contract
This is a **selected reference**, appropriate only when the charter's real valuable action is assigning and reviewing recurring work. It is not a claim that checklist software is a validated market. A tenant is one paying organization with one or more verified users. The React browser calls the Express API; Replit-managed Clerk authenticates people; PostgreSQL holds app state; Stripe hosts card collection and billing management. The Express process is the sole database and Stripe caller. Use separate development and production identity, database, Stripe and host settings. Never send a database password, Stripe secret or webhook secret to the browser or an agent prompt.
### Complete v1 journey
An owner signs in and creates a workspace. The server creates the workspace and owner membership atomically from the **verified server identity**, without accepting `ownerUserId` from the browser. The owner defines a checklist template with a name, instructions and at least one item, invites a member by email, and assigns a dated run. The invited person accepts using the intended verified email address, opens only their workspace's run, marks items done with a note, and submits it. The owner sees the submitted run and either accepts it or returns it with a reason. Both users can inspect the saved status/history after refresh. This is the useful core action, including its feedback loop.
Only one workspace membership per verified user and one paid subscription per workspace are supported in this reference. A real product needing cross-workspace consultants, ownership transfer, team billing splits, files, offline work, public sharing or regulated evidence needs a different approved contract. Set a real data retention and export policy before using real records.
### Identity and access matrix
The server verifies the Clerk session on every app route except the signed Stripe webhook and deliberately public read-only pages. **Three session-only, unenrolled-user exceptions** are `GET /api/me`, `POST /api/workspaces` and `POST /api/invitations/accept`: they require a verified session (and the shared Origin/CSRF/body controls for the two mutations), but cannot require membership before creating it. All other private routes require a current active PostgreSQL membership. Tenant ID in a path is a selector, never authority. Scope every read and write to authenticated membership and the row's workspace ID. Mutations use the common workspace transaction/lock protocol below. Do not trust browser role, email, price or workspace metadata. Cross-tenant resource IDs return the same 404 as nonexistent IDs. Signed-out requests get 401; a known member without the role gets 403.
| Action | Owner | Member | Signed out / other workspace |
| --- | --- | --- | --- |
| View private workspace, template and run | Own workspace | Own workspace only | Denied |
| Create/update template and assignment | Own workspace | Denied | Denied |
| Mark assigned run item done; submit run | Only if assigned | Only if assigned | Denied |
| Accept or return submitted run | Own workspace | Denied | Denied |
| Create/revoke invitation; deactivate member | Own workspace | Denied | Denied |
| Create hosted Checkout or billing portal session | Own workspace owner | Denied | Denied |
| Receive Stripe webhook | Verified Stripe signature only | No browser route | No browser route |
The invitation stores a SHA-256 hash of a cryptographically random 32-byte token, a normalized invited email, workspace, expiry and consumed/revoked timestamps. Return the one-time URL only to the authenticated creating owner for sharing through an authorized channel; never display it again or log it. An unused, unrevoked and unexpired invitation for the same workspace/email returns 409 rather than silently creating another. Invitation issuance is a one-time-secret exception to generic response replay: keep only its hash, not the token in mutation\_keys. If creation response is lost, the owner reads the token-free list, revokes that exact unused invite and issues a new one under the workspace lock. On acceptance, verify the signed-in Clerk user's **verified** primary email equals the invited email; an email query parameter is never authority. Under the user-then-workspace lock protocol, reject expired/used/revoked tokens or membership in another workspace. An already-active same-workspace member returns 409 without consuming the token. A formerly inactive same-workspace member may rejoin only through a fresh owner invitation: reactivate that retained membership row, append membership history and consume the token in one transaction. A removed member's old provider session never restores app access. The owner may not deactivate themselves; ownership transfer is outside v1.
### Persistence and state
Use the SQL below as the new-project reference. It defines workspaces, members, invitations, versioned templates/items, runs/items, run and membership history, mutation keys, billing mappings and webhook states. Each tenant-owned table carries `workspace_id`; compound foreign keys prevent a row from pointing at another tenant's parent. Browser code never connects to SQL or reads billing/event tables. All application state changes use the one-connection transaction protocol below. Check affected-row count and expected current status/version before commit. A stale version returns 409 and the browser reloads the scoped record.
Run state: `assigned -> in_progress -> submitted -> accepted`, with `submitted -> returned -> in_progress` for a review correction; owner cancellation from `assigned`, `in_progress` or `returned` moves to terminal `cancelled`. Only an active assigned member may edit items while the run is `assigned`, `in_progress` or `returned`. The first successful item edit moves `assigned`/`returned` to `in_progress`; an explicit resume moves `returned` to `in_progress` even if items remain complete; `submitted`, `accepted` and `cancelled` items are immutable. Submission requires every required item complete and only `in_progress`; only an active owner reviews `submitted`, with a nonempty reason for return. A successful state change writes one run-history row and increments run version in the same transaction; failed or repeated requests write none. Owner deactivation of a member is blocked with 409 while that member has an assigned, in\_progress, returned or submitted run. Cancel the first three with a reason and create a fresh assignment for an active member; first accept or return a submitted run, then cancel if returned. Historical accepted/cancelled rows remain scoped to the workspace; deactivation does not rewrite them.
### API routes and response contract
`GET /health` exposes only service readiness. Public landing/plans and provider-owned sign-in, callback and auth transport remain outside app membership while carrying no app records. Place Replit's generated Clerk proxy/middleware **before Express parsers/private routes**, preserving the provider's required transport/bootstrap behavior. Mount `POST /api/stripe/webhook` with raw body before general JSON parsing; it is outside app session/CSRF but requires a valid Stripe signature. Every browser state change, including the two unenrolled exceptions, accepts a bounded JSON body and an allowed Origin/CSRF policy matched to the actual cookie/session transport; disallow simple cross-origin form mutation, unsafe GET writes and credentialed wildcard CORS. Each private response, including export and errors, sets `Cache-Control: no-store`. Public/auth transport responses have their separate provider controls.
| Route | Input | Output / invariant |
| --- | --- | --- |
| `GET /api/me` | Verified session, membership optional | No-store active workspace ID/role or workspaceId:null; recovers lost bootstrap/accept response without token replay. |
| `POST /api/workspaces` | Reviewed name; verified session, no prior membership | Session-only bootstrap. Under user lock, create one workspace/owner membership atomically. Same active owner and identical name returns existing workspace; changed name or member/inactive row returns 409. |
| `GET /api/workspaces/:workspaceId` | Path selector | Active member gets only workspace ID/name and own role; never billing secret or other member email. |
| `GET /api/workspaces/:workspaceId/members` | Bounded page | Owner gets member IDs, roles, active flags and safe display names; member gets only self. No invitation tokens. |
| `POST /api/workspaces/:workspaceId/invitations` | Email, bounded expiry | Owner gets 201 invitation ID/expiry and one-time URL; active duplicate email gets 409. Token never in list/replay store/log. |
| `GET /api/workspaces/:workspaceId/invitations` | Bounded page | Owner gets ID, redacted/approved email, expiry and used/revoked status only. |
| `POST /api/workspaces/:workspaceId/invitations/:invitationId/revoke` | Mutation key | Owner atomically revokes an unused invitation; consumed/revoked returns 409. |
| `POST /api/invitations/accept` | One-time token; verified session without membership | Session-only exception. Verified-email match, user/workspace locks, atomic consume and insert/reactivate same-workspace membership; 409 for other-workspace or active membership. |
| `POST /api/workspaces/:workspaceId/members/:userId/deactivate` | Reason, mutation key | Owner only, never self; 409 until assignee's assigned/in\_progress/returned runs are cancelled and submitted runs accepted or returned then cancelled. Sets active=false and appends membership history atomically. |
| `GET /api/workspaces/:workspaceId/templates` and `GET .../templates/:templateId/versions/:version` | Bounded list or exact ID/version | Active member gets ID/version/name/instructions and ordered item position/title/required only; owner also sees archive status. |
| `POST /api/workspaces/:workspaceId/templates` and `POST .../templates/:templateId/versions` | Name/instructions/ordered items, mutation key; expected latest version for update | Owner creates immutable version 1 or appends latest+1 under workspace lock; no in-place rewrite of earlier versions or assigned runs. |
| `POST /api/workspaces/:workspaceId/runs` | Exact template ID/version, active assignee, due date, mutation key | Owner only; 201 run with copied items and initial history. Requires billing-contract paid check immediately before guarded commit. |
| `GET /api/workspaces/:workspaceId/runs` and `GET .../runs/:runId` | Bounded filters/page or exact ID | Owner sees tenant runs; member sees assigned runs only. Detail includes item **position** (not an invented item ID), title, required/done/note, run status/version/due date. |
| `GET /api/workspaces/:workspaceId/runs/:runId/history` | Bounded page | Same scoped run visibility; allowlisted transition/time/actor/reason, no tokens or billing payload. |
| `PATCH /api/workspaces/:workspaceId/runs/:runId/items/:position` | Positive integer position, done, bounded note, expected run version, mutation key | Active assigned member, editable statuses only; scoped item update, run version increment and first-edit transition/history atomically. |
| `POST /api/workspaces/:workspaceId/runs/:runId/resume` | Expected version, mutation key | Assigned active member moves only returned to in\_progress; one history/version change. |
| `POST /api/workspaces/:workspaceId/runs/:runId/submit` | Expected version, mutation key | Assigned member, only `in_progress` with all required items done; one submitted state/history. |
| `POST /api/workspaces/:workspaceId/runs/:runId/review` | `accept`/`return`, return reason, expected version, mutation key | Owner only, submitted only; atomic state/version/history. |
| `POST /api/workspaces/:workspaceId/runs/:runId/cancel` | Reason, expected version, mutation key | Owner cancels only assigned/in\_progress/returned work; history preserved; new run required for reassignment. |
| `GET /api/workspaces/:workspaceId/export` | Owner; bounded cursor and format `json` or `csv` | Even after payment lapse, owner exports only this workspace's templates, runs, items, history and membership audit; no invitation tokens, payment secrets or another tenant. Stream in bounded pages, no-store; audit export privately. |
| `POST /api/workspaces/:workspaceId/billing/checkout` | Owner; fixed plan code and mutation key | Uses billing-contract durable attempt and current-subscription guard; response is validated hosted URL, same-key replay, or pending/hold. No browser Price/Customer ID. |
| `POST /api/workspaces/:workspaceId/billing/portal` | Owner; no Price/Customer input | On-demand short-lived URL for stored workspace Customer only. |
| `POST /api/stripe/webhook` | Raw payload/signature | Verify exact endpoint version/signature; idempotent state and serialized current-resource reconciliation. No callback grant. |
The React UI must expose the whole route table coherently: public landing/plans and provider sign-in; a verified new-user setup screen with create-workspace and accept-invitation alternatives; owner template list/detail/version editor, members/invitations with revoke/deactivate and stranded-work guidance, run list/detail/review/cancel, billing/account/export; and member assigned-run list/detail/item completion/resume/submission with returned reason. Every private view handles loading, empty, 401, 403, 404, 409 reload, payment 403 and provider 503 explicitly. A hidden control is convenience, not the authorization check. After refresh, each view reads the saved API state; a disabled payment action and informational return page never claim payment success. The selected app has no public customer-record view.
#### One mutation order and result contract
Use a **single checked-out PostgreSQL connection** for `BEGIN ISOLATION LEVEL READ COMMITTED`, lock, all reads/writes and `COMMIT`; never let a pool send these statements on different connections. For bootstrap and invitation acceptance, take a transaction advisory lock keyed by the verified Clerk user ID **first**. Bootstrap then checks the unique membership user ID and inserts workspace/member or returns its existing immutable workspace after a retry. Invitation acceptance next reads the token hash only to locate its workspace, takes the workspace advisory lock, then freshly locks/re-reads invitation and membership and applies the email/expiry/active rules, comparing `clock_timestamp()` to `expires_at` under the lock. All other tenant mutations, including invitation issue/revoke, deactivation, template/run/item/review/cancel, billing mapping and webhook reconciliation, take the same workspace advisory lock **before** any membership/resource decision; then freshly re-read active membership and target rows with `FOR UPDATE` when writing. Never acquire a user lock after a workspace lock. PostgreSQL `READ COMMITTED` gives the post-wait statement a fresh snapshot; [transaction isolation](https://www.postgresql.org/docs/current/transaction-iso.html) documents the distinction. The common lock serializes deactivation with later app writes: if removal commits first, the writer's fresh active-membership check fails; if a writer commits first, removal sees its new run state and may return 409. Keep lock scope small and use a bounded statement/lock timeout.
For every ordinary post-bootstrap mutation except invitation issuance, under that lock insert a `(workspace, actor, route, idempotency key)` row with a hash of normalized request fields **before effects**. A committed same-key/same-hash row returns its stored HTTP status and allowlisted response without repeating an effect; same key with different hash returns 409. A visible committed `processing` row violates this atomic contract: HOLD and inspect the exact record/history rather than executing a retry; an uncommitted row rolls back on crash. Store result status/body and state/history in the same commit. Check expected version and allowed `from_status` in the actual `UPDATE ... WHERE workspace_id=? AND id=? AND version=? AND status IN (...) RETURNING` statement; require one returned row. Item PATCH additionally uses the positive `position` composite key and must not write after submission. A failed transaction rolls back key, item and history together. A lost HTTP response is resolved by fresh read of its key and row/history; on deadlock/serialization error retry the **whole** transaction at most twice with the same key after re-reading state, then return 503/409 for operator or user recovery. These are proposed invariants; the owner's real concurrency tests determine whether generated code meets them.
Read routes use exact workspace filters and a fresh active-member lookup on each request. Response objects are explicit allowlists: workspace ID/name/own role; template IDs/version/text and item position; run IDs/status/version/due date/assignee, item fields and history actor/reason; invitation list without token; export without secrets or Stripe payloads. Return only JSON or CSV encoded safely for the chosen export format, with bounded cursor pagination, `no-store` and content disposition. Do not spread SQL `SELECT *` or provider objects to the browser.
The success and cancel return routes are informational. Payment entitlement follows **the billing contract below** using the stored workspace-to-Customer mapping and current matching subscription, plan, invoice and card-payment evidence. For the reference v1, support one fixed monthly card plan, automatic collection and no trials, coupons, prorations or delayed methods. Stripe `active` alone and `checkout.session.completed` alone do not prove a settled current card invoice. Access may pause during payment failure; decide and disclose this policy to customers. A later refund/dispute requires explicit policy and reconciliation. Do not directly set `paid=true` from a client callback. Keep the owner able to view/export existing records when billing lapses under the product's disclosed policy; paid creation is the gated action here.
### Build and release boundaries
Development uses provider test identities, Stripe test mode, a development database and synthetic checklist data. Record actual package versions and supported Node runtime; verify Replit's generated identity setup and deployment type against them. The owner reviews migration and code, tests all routes directly and through the UI, and compares Stripe test Dashboard objects with app billing state. Production uses the separately provisioned Replit-managed Clerk production instance, its own database, Stripe live keys/webhook endpoint and fixed live Price, configured only after business, tax, terms, email, data and payment decisions. Review any automatic deployment branch and host audience before publishing. A successful build, preview URL or Checkout redirect is not proof of a safe SaaS release.
## Single-plan card billing contract
The checked [Stripe API version guide](https://docs.stripe.com/api/versioning) calls **`2026-09-30.endive`** current on 22 October 2026. Pin this **request** version in the installed Node SDK and verify that SDK accepts it; separately create/read the test webhook endpoint and verify its **event** `api_version` is also `2026-09-30.endive`. These are two version settings. If the actual account or SDK cannot use this shape, HOLD and rederive/test the adapter from that account's version; never silently accept an older field layout. No Stripe account, test payment or generated server was run for this packet.
### First customer and Checkout attempt
All owner billing mutations use the app contract's single checked-out `READ COMMITTED` transaction and workspace advisory lock; authorize active **owner** after taking it. Before Checkout, ensure one Stripe Customer for this workspace. Insert `billing_accounts` with an immutable random `customer_setup_key`, exact stored request digest/parameters, random `customer_call_owner`, `customer_call_generation=1`, immutable `customer_first_call_at` and a separate short `customer_call_deadline`, in `reserved` state; commit before calling `stripe.customers.create` with that key and server-set workspace metadata. The first-call timestamp starts the provider-key retention window and never resets on takeover; the deadline only bounds local worker ownership. The owner may persist a returned Customer ID only through the conditional-write protocol below, after checking live mode, ID and that it is not mapped elsewhere. A lost response or first 500 is uncertain, not proof of absence. Automatic recovery may repeat only the identical endpoint, parameters and key while `clock_timestamp() < customer_first_call_at + interval '24 hours'`; after that, HOLD for operator readback of the exact provider request/customer. Never issue a fresh Customer creation key to clear uncertainty; no Checkout starts until the mapping is proven. [Stripe idempotent requests](https://docs.stripe.com/api/idempotent_requests) documents saved first results and possible pruning after 24 hours.
Under the workspace lock, list the **mapped Customer's** subscriptions with `status:'all'`, a bounded `limit:100`, and reject `has_more`/provider uncertainty. Treat every status except `canceled` and `incomplete_expired` as **nonterminal**, including `incomplete`, `trialing`, `active`, `past_due`, `unpaid` and `paused`; if any exists, refuse a new Checkout with 409 and direct the owner to the current billing/recovery path. Also refuse while any attempt is `reserved`, `open`, `unknown` or `hold`, enforced by SQL's partial unique index. One explicit attempt ID and request digest bind workspace, verified owner, Customer, fixed server Price, one-month interval/quantity, card-only method, allowed origin/success/cancel URLs and mode. A new key with changed or existing unresolved binding is 409; an identical key/hash replays its stored open Session URL only after retrieving it and confirming its exact ID/Customer/Price/binding and `to_timestamp(session.expires_at) > clock_timestamp()` on the SQL server. An identical `reserved` attempt returns 202 pending while its owner deadline has not passed; other keys return 409. After a crash/deadline, a recovery worker may take over only with the same immutable binding and key, under the workspace lock, while still inside the immutable first-call retention window. It atomically replaces `call_owner`, increments `call_generation`, and sets a new short `call_deadline` only if state is `reserved` or `unknown`, the old owner/generation still match and `clock_timestamp() >= call_deadline`. A zero-row update means read back and stop. Customer setup uses the identical takeover rule on its own fields; `mapped`, `open`, `completed`, `expired` and `hold` cannot be taken over.
Commit `reserved` with random `call_owner`, `call_generation=1`, immutable `first_call_at` and separate short `call_deadline` **before** Stripe create. The single worker owning it calls `stripe.checkout.sessions.create` in `subscription` mode with stored Customer, `line_items:[{price: fixedPriceId, quantity:1}]`, `payment_method_types:['card']` (and verify account behavior), `client_reference_id` equal to the unpredictable `attempt_id`, metadata containing that same ID, and only approved return origins; use a Stripe idempotency key derived from that immutable attempt. A worker carrying its captured owner UUID and generation may turn `reserved` or `unknown` into `open` on a verified matching response, or `reserved` into `unknown` on timeout/error, **only** by the guarded conditional write below. An unknown response retains the original key, Customer, attempt ID and first-call time. Retry the identical provider request/key only while `clock_timestamp() < first_call_at + interval '24 hours'`; after that, stop automatic retries and use Stripe request logs plus exact signed Session event/readback to resolve it. No new paid attempt is allowed until the unknown one is shown expired or completed. A known open Session becomes `expired` only after retrieving that exact Session and confirming expiry/expiration; a completed Session is terminal only after its subscription/Customer mapping is reconciled. A saved URL or browser return alone never grants access.
**Local completion fence.** Every Customer or Checkout provider-call success, timeout and error finishes in a new one-connection workspace-locked transaction and attempts a single `UPDATE ... RETURNING` against that exact row. The `WHERE` must include workspace and immutable request binding (Customer setup key/digest, or attempt ID/key/digest/Customer/Price/origin and the mode captured by the digest), the worker's captured `call_owner` **and** `call_generation`, and its permitted prior state. Customer success permits only `reserved/unknown -> mapped`; Customer timeout/error permits only `reserved -> unknown` (an already `unknown` result is read back). Checkout success permits only `reserved/unknown -> open`; Checkout timeout/error permits only `reserved -> unknown`. The success update checks the verified response ID against any already stored ID and writes URL/expiry only for that exact Session; a different ID or Customer becomes `hold` only while the same active owner/generation/source state still qualifies, never an overwrite. The corresponding Customer ID uniqueness check and terminal mapping occur in this transaction. The owner writes `unknown` only for uncertain failures; definitive provider errors still require exact readback before a safe terminal disposition and may remain `hold` rather than authorize another key. If `RETURNING` has zero rows, **do not write again**: read back the current row and return its state/result or pending/hold. In particular a webhook-completed, expired, mapped or hold row and a row claimed by another generation cannot be changed by a late success/error/timeout. No worker starts a second provider call on the old generation after takeover. A matching replay against a terminal exact Customer/Session is read-only and idempotent; a different identity on a terminal row leaves that row unchanged and creates a separate operator incident/held event, never a new obligation or entitlement. Never change `completed` or `expired` to `open/unknown`, `mapped` to `unknown`, or any `hold` automatically. The webhook may prove and commit `completed` before the create response arrives; the delayed worker then gets zero rows and reads back `completed`. A takeover may happen before an old worker returns; its old UUID/generation gets zero rows even if the request binding matches.
For example, with `$` placeholders bound from the committed reservation, the Checkout response transition has the following shape. Check `RETURNING` before issuing any follow-up write; the other outcomes use the same full `WHERE` and their narrower state sets. The Customer update substitutes `customer_setup_key`, `customer_request_sha256`, `customer_call_owner`, `customer_call_generation`, and `customer_setup_state` on `billing_accounts`. Use `clock_timestamp()` for takeover/deadline predicates, never transaction-start `now()`.
```sql
update checkout_attempts
set state = 'open', stripe_session_id = $8, stripe_session_url = $9,
stripe_expires_at = to_timestamp($10::double precision), updated_at = clock_timestamp()
where workspace_id = $1 and attempt_id = $2 and idempotency_key = $3
and request_sha256 = $4 and stripe_customer_id = $5
and stripe_price_id = $6 and return_origin = $7
and call_owner = $11 and call_generation = $12
and state in ('reserved','unknown')
and (stripe_session_id is null or stripe_session_id = $8)
returning attempt_id, state, stripe_session_id;
```
A signed `checkout.session.completed` or async event may arrive before the Session ID response was saved. Retrieve its **exact** Session ID from Stripe. It can bind only to one `reserved/open/unknown` attempt with the signed Session's unpredictable `client_reference_id`/metadata `attempt_id`, same mapped Customer, server Price/quantity, subscription mode, deployment `livemode`, return origin and the stored immutable attempt digest; reject a conflicting stored Session ID or a Customer/Subscription already mapped to another workspace. Lock that workspace and attempt, conditionally persist the exact Session ID once and reconcile the current Subscription in the same transaction. This webhook transition uses the immutable binding and allowed source states, but need not match the in-flight create worker's owner token; it must never overwrite a different ID. A matching `completed` replay is read-only/idempotent; `expired` or `hold` is not reopened by a late response/event. Conflicting identities leave a terminal row unchanged and keep the event/incident held for operator comparison; a nonterminal conflict may transition conditionally to `hold` without entitlement. Missing/mismatched evidence cannot enable access. This is a bounded recovery for a lost response, not permission to attach a Customer by email or search alone.
### Signed event processing
Mount `express.raw({type:'application/json',limit:'512kb'})` at the webhook before `express.json()`, after the project's required managed-auth proxy placement. `stripe.webhooks.constructEvent(raw, stripeSignature, endpointSecret)` must succeed; check event `livemode` and `api_version` against this deployment's pinned endpoint version. Invalid signature/version/mode alters no billing mapping and returns an error for investigation. Allow `checkout.session.completed`, `checkout.session.async_payment_succeeded`, `checkout.session.async_payment_failed`, `customer.subscription.created/updated/deleted`, `invoice.paid`, `invoice.payment_failed`, `invoice.voided` and `invoice.marked_uncollectible` as **reconciliation triggers**, never direct grants. Unrelated valid events are recorded `ignored`. Stripe's [subscription webhook guide](https://docs.stripe.com/billing/subscriptions/webhooks) and [delivery guide](https://docs.stripe.com/webhooks) describe asynchronous and unordered delivery.
Insert `event.id` as `pending` once. A concurrent delivery locks that event row so only one processing owner proceeds; a committed `processed`/`ignored` duplicate returns 200. `pending`/`hold` is retried from the **same event** after exact readback, not discarded because its ID exists. Derive candidate workspace only from a verified stored Checkout attempt or a previously immutable Customer mapping. Subscription/invoice events for that Customer arriving before the exact Checkout Session has been proven stay pending: they may not bind a Subscription or process entitlement by themselves. Acquire its workspace advisory lock, then fetch the **current** Subscription/Invoice from Stripe *after* the wait and verify Customer, mode and mapping before updating local UI/cache state. Thus two events cannot commit stale state in reverse order. Persist event result, billing mapping and latest snapshot in one SQL commit. If provider read or mapping is uncertain, leave `pending`/`hold`, return 5xx and let Stripe retry. After delivery retries cease, an owner reconciles stored event ID, exact Session/Customer/Subscription in Stripe Workbench and reprocesses that same record; no new attempt/key or local `paid=true` shortcut. A crashed `processing` transaction rolls back to pending when pending was committed before work; a committed hold remains visible for recovery. Log only IDs/error classes, never webhook payload, signature or card/email data.
### Strict paid-run predicate on `2026-09-30.endive`
The paid action is only `POST /api/workspaces/:workspaceId/runs`; existing scoped reads/export remain available on a disclosed lapse. At the start of the run mutation, take the workspace lock, fresh-check active owner membership and the immutable mapped Customer, and read `SELECT extract(epoch FROM clock_timestamp()) AS current_epoch_seconds` on that same connection. PostgreSQL returns a `numeric` Unix-seconds value here; compare it directly with Stripe's integer Unix-second `period.start/end`, retaining fractional seconds rather than flooring or rounding. For `timestamptz` deadlines (invitation expiry, Customer/Checkout call deadline, Stripe Session expiry), compare to `clock_timestamp()` on the SQL server. Do **not** use `now()`, `CURRENT_TIMESTAMP` or `transaction_timestamp()` for any elapsed-time gate: those are fixed at transaction start, even after a provider wait. Ordinary `created_at DEFAULT now()` audit values can remain. The [PostgreSQL date/time functions](https://www.postgresql.org/docs/16/functions-datetime.html) document the advancing clock and transaction-start alternatives. Perform the following **current provider reads while the workspace lock is held** with a bounded timeout; then fresh-check mapping, membership, advancing clock and expected template/assignee state immediately before the guarded run insert. Holding the lock does not make Stripe and PostgreSQL an atomic system: a provider change just after the read may race with commit. Keep the window bounded, fail closed on provider error, and monitor signed events; claim only the observed decision at this request, never guaranteed irreversible payment.
1. `stripe.subscriptions.list({customer: storedCustomerId, status:'all', limit:100})`: reject `has_more`, unexpected Customer/mode or **more than one nonterminal** subscription across all Prices. Require exactly one, then retrieve that ID. Its status must be `active`, `collection_method==='charge_automatically'`, `pause_collection===null`, `pending_update===null`, no trial, and exactly one subscription item (`items.has_more===false`) with quantity one and **the configured fixed Price**. Do not filter by Price before counting: an extra unrelated active subscription still blocks this one-subscription v1. The current [Subscription object](https://docs.stripe.com/api/subscriptions/object) lists statuses and item `current_period_start/end`.
2. Resolve `latest_invoice` to its exact ID and retrieve it. Require matching `customer`, `livemode`, `collection_method`, `status==='paid'`, `amount_paid>0`, `amount_remaining===0`, expected currency and no credit/discount/manual path outside this reference. Require one unambiguous relevant subscription line with `parent.type==='subscription_item_details'`, `parent.subscription_item_details.subscription===subscription.id`, `subscription_item===currentItem.id`, `pricing.price_details.price===configuredPriceId`, quantity one, and **`line.period.start <= current_epoch_seconds < line.period.end`** with the same start/end as the current subscription item period. Require exactly one invoice line for this selected no-proration/no-extra-item plan. Reject `lines.has_more`, extra invoice lines, missing parent/pricing fields or an invoice only for a previous period. A period boundary at `end` is denied; after a renewal, a new current paid invoice must qualify. The [Invoice](https://docs.stripe.com/api/invoices/object) and [Invoice Line Item](https://docs.stripe.com/api/invoice-line-item/object) references define these fields.
3. `stripe.invoicePayments.list({invoice: latestInvoiceId, limit:100})`: reject `has_more` or ambiguous entries. Require positive `amount_paid` values for `status==='paid'` on exactly that invoice that sum to the Invoice's `amount_paid`, each with `payment.type==='payment_intent'` and an exact PaymentIntent ID. Retrieve each `stripe.paymentIntents.retrieve(id)`, require same Customer/mode and `status==='succeeded'`; resolve its exact `payment_method` through `stripe.paymentMethods.retrieve(id)` and require `type==='card'`. Deny null/missing ID, zero-charge, out-of-band paid mark, customer-balance/credit settlement, delayed method, failed/pending intent or unsupported payment. The [Invoice Payment](https://docs.stripe.com/api/invoice-payment/object), [list method](https://docs.stripe.com/api/invoice-payment/list), [PaymentIntent](https://docs.stripe.com/api/payment_intents/object) and [PaymentMethod](https://docs.stripe.com/api/payment_methods/object) references define the relations.
After all three checks, fresh-read in-transaction membership, mapping, template/version and assignee, then take a **new** `SELECT extract(epoch FROM clock_timestamp()) AS current_epoch_seconds` immediately before the conditional run insert. Require `line.period.start <= current_epoch_seconds < line.period.end` and the same bound for the subscription item's current period. If a provider wait crossed `end`, deny this request and start a new current-state check on a later request; never commit from the earlier clock value. Repeating `SELECT now()` would not detect this crossing. Then insert the run, copied items, initial history and committed mutation-key result atomically. Known ineligibility returns 403 without a run; a provider timeout, page overflow, unknown field or changed mapping returns 503/409 without a run. The browser never receives a Stripe secret, raw provider object or local entitlement flag as authority. A later refund/dispute or reversal needs an explicit owner policy and monitoring. The test matrix must include time just before/at/after period end, **a single transaction whose provider wait crosses period end with unchanged run count**, old paid invoice after failed renewal, extra nonterminal subscription, mismatched Price/customer, manual/credit/zero/non-card payment, changing membership/mapping during provider read, provider outage, duplicate/out-of-order event and unchanged run count after every denial.
Keep test/live keys, webhook endpoint/version, fixed Price, Customer and return origin separate. The portal URL uses only the mapped Customer and is created on demand for the active owner, never cached/shared. If the account cannot enforce this selected card-only one-plan flow, HOLD the paid release and redesign the policy before taking live money. Actual account, SDK response, taxes, cancellation setting and consumer terms remain owner decisions; none were tested here.
## New-project PostgreSQL schema
Review this reference against the generated app and apply it only through a reviewed versioned migration. History reads order by the server-generated `run_history.id` or `membership_history.id`, allocated by an insert after the common workspace lock. These identities define committed event sequence within each workspace; `occurred_at DEFAULT now()` describes transaction-start time and is not a transaction-order key. A rollback can leave gaps. Use workspace-scoped ID cursors and never infer sequence from timestamp alone.
```sql
-- New-project PostgreSQL reference for the selected checklist SaaS contract.
-- Review against the actual generated app and apply through a versioned migration.
-- The Express server is the only database client; never expose its connection string.
create extension if not exists pgcrypto;
create table workspaces (
id uuid primary key default gen_random_uuid(),
name text not null check (char_length(name) between 1 and 120),
created_by text not null,
created_at timestamptz not null default now(),
unique (id, created_by)
);
create table memberships (
workspace_id uuid not null references workspaces(id) on delete restrict,
user_id text not null,
role text not null check (role in ('owner', 'member')),
active boolean not null default true,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
primary key (workspace_id, user_id),
unique (user_id)
);
create unique index one_owner_per_workspace on memberships (workspace_id)
where role = 'owner' and active;
create table membership_history (
id bigint generated always as identity primary key,
workspace_id uuid not null,
member_user_id text not null,
actor_user_id text not null,
action text not null check (action in ('owner_created','invited','reactivated','deactivated')),
reason text,
occurred_at timestamptz not null default now(),
foreign key (workspace_id, member_user_id) references memberships(workspace_id, user_id),
foreign key (workspace_id, actor_user_id) references memberships(workspace_id, user_id)
);
create table invitations (
id uuid primary key default gen_random_uuid(),
workspace_id uuid not null references workspaces(id) on delete restrict,
email_normalized text not null check (char_length(email_normalized) between 3 and 254),
token_hash bytea not null unique,
expires_at timestamptz not null,
consumed_at timestamptz,
revoked_at timestamptz,
invited_by text not null,
created_at timestamptz not null default now(),
foreign key (workspace_id, invited_by) references memberships(workspace_id, user_id),
check (not (consumed_at is not null and revoked_at is not null))
);
create table templates (
workspace_id uuid not null references workspaces(id) on delete restrict,
id uuid not null default gen_random_uuid(),
version integer not null default 1 check (version > 0),
name text not null check (char_length(name) between 1 and 120),
instructions text not null default '' check (char_length(instructions) <= 4000),
archived_at timestamptz,
created_by text not null,
created_at timestamptz not null default now(),
primary key (workspace_id, id, version),
foreign key (workspace_id, created_by) references memberships(workspace_id, user_id)
);
create table template_items (
workspace_id uuid not null,
template_id uuid not null,
template_version integer not null,
position integer not null check (position > 0),
title text not null check (char_length(title) between 1 and 240),
required boolean not null default true,
primary key (workspace_id, template_id, template_version, position),
foreign key (workspace_id, template_id, template_version)
references templates(workspace_id, id, version) on delete restrict
);
create table runs (
workspace_id uuid not null references workspaces(id) on delete restrict,
id uuid not null default gen_random_uuid(),
template_id uuid not null,
template_version integer not null,
assignee_user_id text not null,
due_at timestamptz,
status text not null default 'assigned'
check (status in ('assigned','in_progress','submitted','returned','accepted','cancelled')),
version integer not null default 1 check (version > 0),
created_by text not null,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
primary key (workspace_id, id),
foreign key (workspace_id, template_id, template_version)
references templates(workspace_id, id, version) on delete restrict,
foreign key (workspace_id, assignee_user_id)
references memberships(workspace_id, user_id) on delete restrict,
foreign key (workspace_id, created_by)
references memberships(workspace_id, user_id) on delete restrict
);
create index runs_assignee_status on runs(workspace_id, assignee_user_id, status);
create table run_items (
workspace_id uuid not null,
run_id uuid not null,
position integer not null check (position > 0),
title text not null check (char_length(title) between 1 and 240),
required boolean not null,
done boolean not null default false,
note text not null default '' check (char_length(note) <= 2000),
updated_at timestamptz not null default now(),
primary key (workspace_id, run_id, position),
foreign key (workspace_id, run_id) references runs(workspace_id, id) on delete restrict
);
create table run_history (
id bigint generated always as identity primary key,
workspace_id uuid not null,
run_id uuid not null,
from_status text,
to_status text not null,
reason text,
actor_user_id text not null,
occurred_at timestamptz not null default now(),
foreign key (workspace_id, run_id) references runs(workspace_id, id) on delete restrict,
foreign key (workspace_id, actor_user_id)
references memberships(workspace_id, user_id) on delete restrict
);
create index run_history_tenant_run on run_history(workspace_id, run_id, id);
create table mutation_keys (
workspace_id uuid not null references workspaces(id) on delete restrict,
actor_user_id text not null,
route_key text not null,
idempotency_key text not null,
request_sha256 bytea not null,
result_status integer,
result_json jsonb,
state text not null check (state in ('processing','committed')),
created_at timestamptz not null default now(),
completed_at timestamptz,
primary key (workspace_id, actor_user_id, route_key, idempotency_key),
foreign key (workspace_id, actor_user_id)
references memberships(workspace_id, user_id) on delete restrict
);
-- These tables are never returned through general workspace-record endpoints.
create table billing_accounts (
workspace_id uuid primary key references workspaces(id) on delete restrict,
customer_setup_key text not null unique,
customer_request_sha256 bytea not null,
customer_setup_state text not null check (customer_setup_state in ('reserved','unknown','mapped','hold')),
customer_call_owner uuid,
customer_call_generation bigint not null default 1 check (customer_call_generation > 0),
customer_first_call_at timestamptz,
customer_call_deadline timestamptz,
stripe_customer_id text unique,
stripe_subscription_id text unique,
stripe_price_id text,
subscription_status text,
paid_period_end timestamptz,
current_invoice_id text,
entitlement_checked_at timestamptz,
updated_at timestamptz not null default now(),
unique (workspace_id, stripe_customer_id)
);
create table checkout_attempts (
workspace_id uuid not null references workspaces(id) on delete restrict,
attempt_id uuid not null default gen_random_uuid() unique,
owner_user_id text not null,
idempotency_key text not null,
request_sha256 bytea not null,
stripe_customer_id text not null,
stripe_price_id text not null,
return_origin text not null,
stripe_session_id text unique,
stripe_session_url text,
state text not null check (state in ('reserved','open','unknown','completed','expired','hold')),
call_owner uuid,
call_generation bigint not null default 1 check (call_generation > 0),
first_call_at timestamptz,
call_deadline timestamptz,
stripe_expires_at timestamptz,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
primary key (workspace_id, idempotency_key),
foreign key (workspace_id, owner_user_id) references memberships(workspace_id, user_id),
foreign key (workspace_id, stripe_customer_id) references billing_accounts(workspace_id, stripe_customer_id)
);
create unique index one_unresolved_checkout_per_workspace on checkout_attempts(workspace_id)
where state in ('reserved','open','unknown','hold');
create table stripe_events (
stripe_event_id text primary key,
event_type text not null,
stripe_created_at timestamptz not null,
received_at timestamptz not null default now(),
processed_at timestamptz,
attempt_count integer not null default 0 check (attempt_count >= 0),
last_error_code text,
result text not null check (result in ('pending','processing','processed','ignored','hold'))
);
-- SQL keys preserve tenant relations; they do not replace server authorization.
-- Every private query must derive workspace and role from current verified membership.
-- Migrations, least-privilege database roles, indexes and retention require owner review.
```
## Copyable Replit Agent build brief
Give the agent the filled charter above, this brief, **the application contract above**, **the billing contract above** and **the SQL above** only after the owner has decided that the selected checklist workflow fits the real problem. Keep customer interviews, payment credentials and private records out of the prompt. Replace bracketed decisions with approved facts; do not let the agent fill them imaginatively.
```text
Build a first version of a paid team checklist workspace for [real, owner-validated customer role and task]. The first valuable job is: an owner creates a recurring checklist, assigns a dated run to a verified member, that member completes and submits it, and the owner accepts or returns it with a reason. Use the filled charter and the application, billing and SQL contracts in this guide as binding requirements. Copy those complete sections into this controlled project with the prompt. If the charter contradicts this selected job, STOP and ask for a redesigned contract; do not relabel a form or invent customers, demand, prices, legal terms or test results.
Use React for the interface, an Express server, PostgreSQL, the Replit project's supported managed Clerk authentication, and Stripe-hosted Checkout and Billing Portal. Check the project's generated auth/proxy setup, installed packages, Node runtime and account capabilities before writing code. Plan first. Return a file/route/data map, the proposed migration, the permission matrix, environment list, test plan, budget-sensitive provider actions and explicit unknowns. Wait for my review of that plan before implementation.
Implement the entire selected v1 journey, including session-only unenrolled bootstrap and invitation acceptance, one-workspace membership, invitation revoke/rejoin, member deactivation with stranded-run cancellation, complete template/run/history/export views, versioned templates, copied run items, member submit, owner accept/return, immutable history, server-side fixed Price lookup, hosted billing, signed idempotent webhooks and server-computed paid creation access. Browser role, tenant ID, email, Stripe Customer ID and Price ID are never authority. Use the app contract's single-connection READ COMMITTED user-then-workspace lock order for mutations and fresh active-membership checks after locking. Every other private read/write except session-only `GET /api/me` requires current active membership and scoped SQL. Do not expose the database connection or Stripe secrets in browser code. Follow the exact UI/API/state and response allowlists in the application contract above. Use separate development and production identity, database and Stripe settings. Do not connect a production account, apply a production migration, publish, send real invitations or charge a card without my separate approval.
Use a versioned SQL migration based on the SQL above. Present the whole generated diff and tests. Add direct API tests for signed-out, cross-workspace, member-on-owner-route, removed member, wrong invitation email, expired/replayed invitation, stale version, repeated idempotency key, submitted/accepted item immutability, both orders of revocation versus assignment/item/submit/review, unknown Checkout and duplicate-subscription prevention, Customer/Checkout owner-generation fencing with webhook-before-response/timeout and takeover-before-old-result order tests, unpaid/failed/current-period card entitlement with a provider wait crossing period end, and duplicate/out-of-order Stripe events. Pin and verify request API `2026-09-30.endive` separately from the webhook endpoint event version; if the actual SDK/account differs, stop and redesign the adapter. Include both browser journey and state/readback checks, with unchanged-row/run evidence on every denial. Report which tests actually ran, their environment and results; mark unrun tests as unrun. Do not claim a security audit or live launch.
At the end, give me the exact files changed, migration history, local/development URLs, provider configuration still needed, actual observed tests, unresolved risks, account costs/limits to inspect and a reversible release/rollback checklist. Stop at a reviewable development build.
```
Plan approval is only permission to let the reader's agent edit its controlled development project. The owner separately approves provider accounts, live payment configuration, production migration, production data and publication.
## Owner-run acceptance and release record
Fill results from the real development project. `unrun` is an honest result. Record environment, build/commit, test identities and Stripe test object IDs in the owner's private system; do not paste credentials or customer data into a public document.
| Check | Required observation | Actual result / evidence |
| --- | --- | --- |
| Customer task fit | Named permitted users can complete the full charter action; the selected checklist model genuinely represents it | [pass/fail/unrun, notes] |
| Plan and diff | Owner reads all changed files, dependencies, migrations, generated auth middleware order and secret exposure | - [ ] |
| Identity bootstrap | Verified person creates one workspace; another person cannot claim it | - [ ] |
| Bootstrap concurrency | Same verified user makes concurrent/retried creates: one workspace/owner membership, same ID returned; changed name is 409. Unenrolled invite acceptance still works behind session/Origin/body guard | - [ ] |
| Invitation | Intended verified email accepts once; wrong email, expired, revoked and replayed token all fail | - [ ] |
| Membership lifecycle | Duplicate active invite is 409; revoke blocks acceptance; removed same-workspace member rejoins only with fresh owner invite; other-workspace member is denied; owner cannot remove self | - [ ] |
| Tenant reads | Two real test workspaces A/B; A user cannot read B template/run/history using direct IDs/API | - [ ] |
| Tenant writes | A user cannot modify B item or assign B member; forged browser role/workspace fails | - [ ] |
| Role and removal | Member cannot call owner routes; revoked member's existing session cannot use private API | - [ ] |
| Complete core action | Owner template and assignment → member item completion/submission → owner accept/return → saved state/history after refresh | - [ ] |
| Complete navigation/export | Template list/detail/new version, run detail/items/history, invitation/member views and owner-only scoped JSON/CSV export work after reload and billing lapse; private responses are no-store allowlists | - [ ] |
| Unhappy core paths | Missing required item, stale version, double-click/replayed mutation and concurrent review produce no extra transition | - [ ] |
| Revocation races | Run deactivation before/after assignment, item edit, submit and review in both commit orders. Deactivation blocks on open assigned work until owner cancels/reassigns by new run; no post-removal write; submitted/accepted/cancelled item edit denied with unchanged row/history | - [ ] |
| Mutation recovery | Same-key/same-body returns exact stored response, changed body 409; lost response readback finds one effect; deadlock/timeout bounded retry leaves no extra item/run/history | - [ ] |
| Test billing | Checkout creates intended single test Customer/subscription; portal routes to its Customer; no browser card capture | - [ ] |
| Duplicate Checkout | Existing nonterminal subscription blocks new key; reserved/open/unknown/hold attempt blocks concurrent and sequential new keys. Same key replays only a proven unexpired Session; lost Session ID reconciles through exact attempt/Customer/signed event or remains HOLD | - [ ] |
| Provider-call ownership orders | Deterministically pause/release Customer and Checkout creates: webhook maps/completes before original success and before timeout; takeover commits before old success and before old error; matching terminal event/response replays. Exact binding plus owner UUID/generation and allowed source state fence every worker update. Late worker returns readback only. Assert unchanged mapped Customer/Session/subscription, terminal state, event result and one obligation; conflicts remain held without a new key or entitlement | - [ ] |
| First-call and deadline clocks | Immutable first-call time stays fixed across owner takeovers; only short call deadline and generation advance. Repeat identical provider request/key strictly before first call + 24 hours; at/after retention expiry require operator readback. Compare `clock_timestamp()` to SQL deadlines/expiry | - [ ] |
| Payment boundary | Return URL alone grants nothing; paid current period grants chosen action; failed/unpaid/expired period blocks it; webhook replay/out-of-order reconciles without duplicate entitlement | - [ ] |
| Provider-wait period crossing | Start one READ COMMITTED run transaction before `period.end`, pause a provider read through the boundary, then release it. The fresh `extract(epoch FROM clock_timestamp())` numeric seconds value is at/after end and denies the run; run, copied items, history, mutation result and count remain unchanged. `now()`/`CURRENT_TIMESTAMP` would falsely keep the transaction-start value | - [ ] |
| Payment adapter boundaries | Pinned request/event versions match; test before/at/after period end, extra nonterminal subscription, wrong Price/customer, manual/credit/zero/non-card payment, changed mapping, provider error and duplicate/out-of-order events. Every denial leaves run count unchanged | - [ ] |
| Separate production | Production Clerk, database, Stripe keys, fixed live Price and webhook endpoint are independently verified; no development data or secret copied | - [ ] |
| Release | Owner approves terms, tax/payment policy, audience, migration and exact build; public URL sign-in/private route/paid action and denial checks repeated | - [ ] |
| Operations | Support contact, monitoring, cancellation handling, export/retention, backup/restore route, rollback owner and cost alerts recorded | - [ ] |
If any identity, tenant or payment denial fails, stop the release, preserve the failed request and server trace without secrets, repair the exact route/query/event mapping, then repeat the direct and browser checks. If a migration is uncertain, compare the actual target and migration history before another write. If the deploy is unhealthy, return traffic to the last known good release under the host's supported procedure, preserve schema compatibility and investigate; do not assume code rollback reverses a database migration or a Stripe charge. Label a restore **untested** until rehearsed on an authorized separate target.
## Sources and further reading
The official manuals document platform controls and object fields. The charter, application and billing contracts are proposed designs; the owner must observe the tests in the real account.
- [Replit Plan Mode](https://docs.replit.com/features/agent/plan-mode): plan review before code and data changes; no plan was run here.
- [Replit Agent task board](https://docs.replit.com/references/agent/task-board): task state is separate from owner acceptance.
- [Replit managed Clerk authentication](https://docs.replit.com/features/auth-and-identity/clerk-auth): generated identity and proxy setup; inspect the real project order and version.
- [Replit development and production databases](https://docs.replit.com/features/data-and-storage/development-and-production): distinct data environments; no account inspected.
- [Replit publishing](https://docs.replit.com/features/publishing/overview): deployment controls; no release performed.
- [Replit data recovery](https://docs.replit.com/features/data-and-storage/data-recovery): available recovery route depends on account and environment; no restore rehearsed.
- [Replit AI billing](https://docs.replit.com/billing/ai-billing): owner checks current Agent spending terms and account limits.
- [Clerk Express getAuth](https://clerk.com/docs/reference/express/get-auth): verified session identity; application membership remains a separate database decision.
- [Clerk backend getUser](https://clerk.com/docs/reference/backend/user/get-user): current verified primary email for invitation acceptance; URL-supplied email has no authority.
- [PostgreSQL transaction isolation](https://www.postgresql.org/docs/current/transaction-iso.html): READ COMMITTED post-wait statement snapshots; proposed concurrency protocol untested.
- [PostgreSQL explicit locking](https://www.postgresql.org/docs/current/explicit-locking.html): transaction advisory locks; application behavior must be checked under concurrency.
- [PostgreSQL date/time functions](https://www.postgresql.org/docs/16/functions-datetime.html): advancing `clock_timestamp()` and transaction-start `now()`; strict elapsed-time gate untested.
- [PostgreSQL identity columns](https://www.postgresql.org/docs/current/ddl-identity-columns.html): generated history IDs; the guide's post-lock ordering rule is an original proposal.
- [Stripe API versioning](https://docs.stripe.com/api/versioning): checked request version `2026-09-30.endive` and separately configured endpoint event version; actual SDK/account readback required.
- [Stripe hosted subscription Checkout](https://docs.stripe.com/billing/subscriptions/build-subscriptions.md?payment-ui=checkout&ui=stripe-hosted): provider-hosted customer payment route; no Checkout Session created here.
- [Stripe Checkout create](https://docs.stripe.com/api/checkout/sessions/create): Customer, fixed Price and `client_reference_id` binding; account response unobserved.
- [Stripe idempotency](https://docs.stripe.com/api/idempotent_requests): same-key result and possible key pruning after 24 hours; the proposed local fence needs reader testing.
- [Stripe webhooks](https://docs.stripe.com/webhooks): raw-body signature and asynchronous retry behavior; no endpoint tested.
- [Stripe subscription object](https://docs.stripe.com/api/subscriptions/object): statuses and item period fields used in the proposed paid predicate.
- [Stripe Invoice and line items](https://docs.stripe.com/api/invoices/object): invoice fields; [line item](https://docs.stripe.com/api/invoice-line-item/object) identifies the current subscription item and Price.
- [Stripe Invoice Payment](https://docs.stripe.com/api/invoice-payment/object): paid invoice/payment relationship; [list method](https://docs.stripe.com/api/invoice-payment/list) requires pagination checks.
- [Stripe PaymentIntent](https://docs.stripe.com/api/payment_intents/object) and [PaymentMethod](https://docs.stripe.com/api/payment_methods/object): successful card evidence in the proposed fixed-plan policy.
- [Stripe Billing Portal](https://docs.stripe.com/customer-management/configure-portal): owner-controlled customer billing management; configuration depends on the real account.
## Sources
- [Replit Plan Mode](https://docs.replit.com/features/agent/plan-mode) — plan review before code and data changes; no plan was run here.
- [Replit Agent task board](https://docs.replit.com/references/agent/task-board) — task state is separate from owner acceptance.
- [Replit managed Clerk authentication](https://docs.replit.com/features/auth-and-identity/clerk-auth) — generated identity and proxy setup; inspect the real project order and version.
- [Replit development and production databases](https://docs.replit.com/features/data-and-storage/development-and-production) — distinct data environments; no account inspected.
- [Replit publishing](https://docs.replit.com/features/publishing/overview) — deployment controls; no release performed.
- [Replit data recovery](https://docs.replit.com/features/data-and-storage/data-recovery) — available recovery route depends on account and environment; no restore rehearsed.
- [Replit AI billing](https://docs.replit.com/billing/ai-billing) — owner checks current Agent spending terms and account limits.
- [Clerk Express getAuth](https://clerk.com/docs/reference/express/get-auth) — verified session identity; application membership remains a separate database decision.
- [Clerk backend getUser](https://clerk.com/docs/reference/backend/user/get-user) — current verified primary email for invitation acceptance; URL-supplied email has no authority.
- [PostgreSQL transaction isolation](https://www.postgresql.org/docs/current/transaction-iso.html) — READ COMMITTED post-wait statement snapshots; proposed concurrency protocol untested.
- [PostgreSQL explicit locking](https://www.postgresql.org/docs/current/explicit-locking.html) — transaction advisory locks; application behavior must be checked under concurrency.
- [PostgreSQL date/time functions](https://www.postgresql.org/docs/16/functions-datetime.html) — advancing `clock_timestamp()` and transaction-start `now()`; strict elapsed-time gate untested.
- [PostgreSQL identity columns](https://www.postgresql.org/docs/current/ddl-identity-columns.html) — generated history IDs; the guide's post-lock ordering rule is an original proposal.
- [Stripe API versioning](https://docs.stripe.com/api/versioning) — checked request version `2026-09-30.endive` and separately configured endpoint event version; actual SDK/account readback required.
- [Stripe hosted subscription Checkout](https://docs.stripe.com/billing/subscriptions/build-subscriptions.md?payment-ui=checkout&ui=stripe-hosted) — provider-hosted customer payment route; no Checkout Session created here.
- [Stripe Checkout create](https://docs.stripe.com/api/checkout/sessions/create) — Customer, fixed Price and `client_reference_id` binding; account response unobserved.
- [Stripe idempotency](https://docs.stripe.com/api/idempotent_requests) — same-key result and possible key pruning after 24 hours; the proposed local fence needs reader testing.
- [Stripe webhooks](https://docs.stripe.com/webhooks) — raw-body signature and asynchronous retry behavior; no endpoint tested.
- [Stripe subscription object](https://docs.stripe.com/api/subscriptions/object) — statuses and item period fields used in the proposed paid predicate.
- [Stripe Invoice and line items](https://docs.stripe.com/api/invoices/object) — invoice fields; [line item](https://docs.stripe.com/api/invoice-line-item/object) identifies the current subscription item and Price.
- [Stripe Invoice Payment](https://docs.stripe.com/api/invoice-payment/object) — paid invoice/payment relationship; [list method](https://docs.stripe.com/api/invoice-payment/list) requires pagination checks.
- [Stripe Billing Portal](https://docs.stripe.com/customer-management/configure-portal) — owner-controlled customer billing management; configuration depends on the real account.
- [Stripe PaymentIntent object](https://docs.stripe.com/api/payment_intents/object) — Exact successful intent, Customer, live mode and payment method relationships for the proposed current paid-card predicate; no account readback or payment performed.
- [Stripe PaymentMethod object](https://docs.stripe.com/api/payment_methods/object) — Card type readback for the proposed fixed-plan paid predicate; actual provider object and account behavior remain untested.
The BIG CHANGE newsletter
The big picture. At your pace.
Recent stories on AI and robotics, the shifts worth watching and practical ideas to use. Choose a daily briefing, weekly digest or monthly perspective.
Next scheduled send (UTC): . Your first edition arrives at the next scheduled send after you confirm.
Your privacy, your choice.
Necessary storage supports site security and remembers your choices. Optional Google Analytics stays off until you allow it. You can read every story with necessary storage only. Privacy details