Personas
▾Four people touch the system. Three are DB roles (app_role:
customer · editor · admin); the "premium editor" is an
operational label, not a separate role.
Real-estate media company
Owner of a small media business who shoots raw property footage (drone, walkthroughs, agent on-camera) but doesn't want to edit. Buys credits in bulk and submits jobs.
Goal: a finished, branded property video back in 48h (24h with the Rush add-on) — trackable, downloadable, revisable. Gets in via the invite-only waitlist; every order is paid (no free starter credits).
Standard editor
Internal/contract editor invited by the admin. Handles the bread-and-butter Standard packages.
Goal: see assigned orders in a queue, work them before the due date, deliver the file, and turn revisions around fast.
Premium editor
Same role, trusted with Premium/longer-form and advanced add-ons (on-camera advanced, AI
effects, voiceover). editor_specialty exists but is display-only —
auto-assignment routes by load, not specialty.
Goal: same as standard editor, plus premium-tier work. Specialty-aware routing is a planned enhancement.
Admin / Operator
The launch operator and hub of the system: oversees the auto-assigned queue (and reassigns/ overrides when needed), manages the editor roster and customers, grants/reconciles credits, handles contact inquiries.
Goal: keep the queue moving — auto-assign puts every paid order on an editor's queue at submit; the admin catches the exceptions (no eligible editor, over-capacity, reassignment).
customer. Roles live in a separate
user_roles table, checked via the has_role(uid, role) SECURITY DEFINER
function — never as a column on profiles.
End-to-End Journeys
▾2.1 — Customer Journey
Stripe embedded checkout] D --> E[Submit edit request
submit_edit_order RPC] E -->|Auto-assigned at submit
least-loaded active editor under capacity| F[Order: awaiting_footage
edit footage_link here only] F --> G[Editor works it
accepted → in_progress] G --> H[Order: in_progress
email: order-status-update] H --> I[Order: ready_for_delivery
email: delivery-file-added] I --> J[Download via signed URL] J --> K{Satisfied?} K -->|Yes| L([Delivered]) K -->|No| M[Request revision
request_revision RPC] M --> H F -->|Cancel while awaiting| N([Cancelled — credits restored])
Discover → Waitlist → Sign up (invite-only beta)
- Discover. Public marketing pages —
/,/pricing,/how-it-works,/ai-examples,/faq. During the invite-only beta the primary CTA points to/waitlist. - Join the waitlist.
/waitlistembeds a Fillout form (WAITLIST_URL). This is the front door: prospects request access and the team reaches out to invite them when a spot opens. There is no invite-code field in the app — access is gated by who gets invited off the waitlist, not by a code typed at signup. - Create the account at
/signuponce invited. The visitor must download the Upload Guide PDF and tick the confirmation box before the buttons enable. Required: full name, email, password (min 8) — or Continue with Google (OAuth signup is available). No invite code is entered here. - Server-side account creation.
signUp()callssupabase.auth.signUp. Onauth.usersINSERT thehandle_new_user()trigger creates the profile +customerrole and grants the currentstarter_creditsfromapp_settings— set to 0 for launch, so new customers start with an empty balance and must buy credits before their first edit. (Comped testers are topped up by an admin via the customer credit-grant tool.)
Verify email
- Verification email (Supabase Auth via Lovable email infra,
signuptemplate). The page swaps to a "Verify your email" panel. - Click the link → redirected to
/dashboard. The_authenticated.tsxlayout gates everything under it.
Buy credits
- Go to
/buy-credits. Seven USD packs (100 → 10,000 credits) + Annual Partner plan, with volume discounts. - Stripe embedded checkout → on completion redirects to
/checkout/return?session_id=…. - Credits granted server-side.
checkout.session.completed→payments/webhook: idempotency-guards viacredit_transactions, maps pricelookup_key→ credits, inserts a transaction, incrementsprofiles.credit_balance. Noordersrow is created for a credit buy. Balance is server-authoritative. - Return page calls
checkStatusto verify the real session status (no more false green check).
Submit an edit request (3-step checkout wizard)
- Go to
/submit. The order form is a 3-step wizard (aStepIndicatorat the top):- Step 1 — Your details. Business/contact details (a
StandingDetailsCardprefills returning customers) and the required delivery email. - Step 2 — Order specifics. Package (Standard 30s / 60s, Premium 60s / 120s); clip count (extras bill at 5 credits each over the package's included clip limit); format (Reel/Horizontal/Both); add-ons (intro-outro, on-cam advanced, captions, voiceover, Rush — 24h, AI effects 1×/3×/5×, property lines); footage link, branding link, reference video link, music, on-screen text, notes.
- Step 3 — Review & confirm. A line-item credits summary shows package + add-ons + clip overage vs. the current balance before the customer confirms.
- Step 1 — Your details. Business/contact details (a
- Confirm →
submit_edit_orderRPC. Client sends labels and canonical slugs; costing is 100% server-side frompackage_prices/addon_prices(unknown slugs rejected). The RPC deducts credits, setsdue_at= now + 24h if the Rush add-on is present, else 48h, inserts the order atawaiting_footage/payment_status='paid'/currency='USD', writes a debit transaction, and then callsauto_assign_order()(see the Editor journey). Returns the order id → navigates to/dashboard/orders/:id.
Track → Deliver → Download
- Order starts at
awaiting_footage, already assigned. Auto-assignment normally picks an editor at submit; the order lands on that editor's queue immediately (it only stays unassigned if no active editor has capacity). The customer may edit onlyfootage_link/special_instructionshere (enforced by theorders_restrict_customer_updatestrigger). - Track at
/dashboard(credit balance, order list,StatusGauge) and/dashboard/orders/:id. Status emails fire onin_progressandready_for_delivery(order-status-update). - Receive delivery at
/dashboard/orders/:id/deliverywhen status hitsready_for_delivery; each file also firesdelivery-file-added. - Download via a 10-minute signed URL from the
deliveriesbucket; logged toorder_delivery_downloads.
Request a revision → revision loop
- Request a revision →
request_revisionRPC (up to 20 attachments →order_revision_attachments). Moves the orderdelivered → in_progressand bumpsrevision_count— the only customer-driven status change RLS whitelists. - Editor notified (
revision-requested). The editor/admin drives sub-stages viaset_revision_stage(queued → in_progress → complete), notifying both parties (revision-progress). The loop can repeat.
2.2 — Editor Journey
inviteEditor RPC]) --> B[Set password via invite link] B --> C[Log in at /login] C --> D[Set availability
set_editor_availability RPC] D --> E[Auto-assigned an order at submit
auto_assign_order: least-loaded, in-app notification] E --> F[View queue at /editor/index
getEditorQueue: my orders by due_at] F --> G[Open order workspace
/editor/orders/:id] G --> H[Move: accepted] H --> I[Move: uploading_footage] I --> J[Move: in_progress
email: order-status-update to customer] J --> K[Upload delivery files
deliveries bucket + notifyDeliveryUploaded] K --> L[Mark: ready_for_delivery
email: delivery-file-added] L --> M[Mark: delivered] M --> N{Revision requested?} N -->|Yes — customer triggers| O[Work revision
set_revision_stage] O --> L N -->|No| P([Done]) E -->|Due-soon cron ~6h| Q[Due-soon email alert]
- Invited (no self-signup). Admin runs
inviteEditor→auth.admin.inviteUserByEmail, grants theeditorrole, setsis_editor_active=true. Editor sets a password via the invite link, logs in at/login. - Set availability at
/editor/availability→setEditorAvailability. The two privileged fields (is_editor_active,max_concurrent_orders, default 5) are written through theset_editor_availabilitySECURITY DEFINER RPC (a trigger + column REVOKE block direct writes); specialty/bio go through the normal user update. - Receive an assignment (usually automatic). When a customer submits, the
submit_edit_orderRPC callsauto_assign_order(), which — while theauto_assignflag inapp_settingsis on (it is) — picks the least-loaded active editor still undermax_concurrent_orders(row-locked withSKIP LOCKEDto avoid double-booking), setsassigned_to, and inserts anorder_assignedin-app notification. An admin can still runassign_ordermanually to (re)assign; that path also enqueues theeditor-assignmentemail. - See the queue at
/editor/index→getEditorQueue: orders whereassigned_to = meand not cancelled, sorted bydue_at. Overdue =due_at < now. - Work the order at
/editor/orders/:id, which redirects to the shared, role-aware/admin/orders/:idworkspace. RLS scopes editors to their assigned order. - Move status forward (
accepted → uploading_footage → in_progress) viaupdateOrderStatus;in_progressemails the customer.assertCanEditOrderallows only the assigned editor or an admin. - Upload the delivery to the
deliveriesbucket (rows inorder_deliveries);notifyDeliveryUploadedemails the customer per file. - Mark ready / delivered.
ready_for_deliveryunlocks the delivery page;deliveredstampsdelivered_at. - Handle revisions —
revision-requestedemail, work it, driveset_revision_stage. - Due-soon alerts. A Bearer-protected cron hook
(
api/public/hooks/editor-due-soon) scans orders due within ~6h and emails the assigned editor (editor-due-soon), de-duped viadue_soon_alerted_at.
auto_assign_order) and the manual assign_order RPC both pick
on current load + capacity among active editors — neither matches on
editor_specialty. Premium vs. standard remains an operational label; specialty-aware
routing is a planned enhancement.
2.3 — Admin / Operator Journey
auto-assigned at submit]) --> B[/admin/index orders board
all orders, filterable/] B --> C{Needs action?} C -->|Reassign / no editor free| D[Open /admin/orders/:id] D --> E[(Re)assign editor
assign_order RPC — manual override] E --> F[Adjust due date
updateOrderDueDate] F --> G[Monitor progress
order_status_history audit trail] G --> H{Issue?} H -->|Notes / override| I[Write internal notes
Change status if needed] H -->|No issue| J([Order delivered]) C -->|Inbox| K[Triage contact submissions
/admin/inbox — Gmail threads] C -->|Editors| L[Manage editor roster
/admin/editors] L --> M[Invite / remove editors
inviteEditor, removeEditorRole] L --> N[Check workload
editor_workload_summary RPC] C -->|Customers| O[Customer detail
/admin/customers/:id] O --> P[Grant / deduct credits
adminAdjustCredits] C -->|Payment issue| Q[Reconcile a Stripe session
/admin/reconcile]
- Orders board at
/admin/index— all orders, filterable by status / "mine" / overdue. Links to inbox and the due-soon console. - Triage the inbox at
/admin/inbox— reads Gmail threads viainbox.functions. The contact form writes acontact_submissionsrow and relays an email toinfo@summitediting.co. - Open an order at
/admin/orders/:id— the central workspace (shared with editors, role-aware; admin-only actions are hidden from editors). From here: (re)assign, set due date, change status, upload deliveries, drive revision stages, write internal notes (order_internal_notes), and cancel & refund. - Reassign / handle the exceptions. Auto-assign already put most orders on an editor's queue
at submit, so the admin's job here is the exceptions: reassign via
updateOrderAssignment→assign_orderRPC (requires admin, validates an active editor, soft-warns on over-capacity but still assigns, setsassigned_to, notifies the editor), and pick up any order that auto-assign left unassigned because no editor had capacity. - Manage editors at
/admin/editors(Editors tab):inviteEditor,removeEditorRole(refused while the editor has active orders), andeditor_workload_summary(). Editormax_concurrent_ordershere is what auto-assign's capacity cap reads. - Manage customers at
/admin/customers(Customers tab) → customer detail (/admin/customers/:id): view a customer's orders and balance, and grant or deduct credits (adminAdjustCredits) — this is how comped testers are topped up now that starter credits are 0. - Reconcile a payment at
/admin/reconcile(adminReconcileSession): paste a Stripesession_idto credit a purchase that the webhook missed (idempotent — a session already processed is a no-op). - Monitor due-soon emails at
/admin/due-soon-emails(Due-soon emails tab). Note: customers self-cancel only whileawaiting_footage; admins can cancel & refund from the order workspace.
Order Lifecycle State Machine
▾States are the order_status enum. Orders enter at
awaiting_footage and are auto-assigned to an editor at submit (the status is
unchanged — assignment just sets assigned_to). Customers can only move
delivered → in_progress (revision) and awaiting_footage → cancelled
(self-cancel); an admin can also cancel & refund from the order workspace. Everything else is
admin/editor-driven. There is no illegal-transition guard yet.
auto_assign_order sets assigned_to awaiting_footage --> cancelled : cancel_order (customer / admin) awaiting_footage --> accepted : admin / editor accepted --> uploading_footage : admin / editor uploading_footage --> in_progress : admin / editor
email: order-status-update in_progress --> ready_for_delivery : admin / editor
email: delivery-file-added ready_for_delivery --> delivered : admin / editor
email: order-status-update delivered --> in_progress : request_revision (customer)
revision_count++ cancelled --> [*] delivered --> [*]
Transition table — who triggers what, which email fires
| From | To | Who | Mechanism | Email(s) |
|---|---|---|---|---|
| — | awaiting_footage | Customer | submit_edit_order (→ auto_assign_order) | — (credits deducted; editor gets in-app order_assigned) |
awaiting_footage | cancelled | Customer | cancel_order | order-cancelled; order-cancelled-editor if assigned |
awaiting_footage | accepted | Admin/Editor | updateOrderStatus | — |
accepted | uploading_footage | Admin/Editor | updateOrderStatus | — |
uploading_footage | in_progress | Admin/Editor | updateOrderStatus | order-status-update |
in_progress | ready_for_delivery | Admin/Editor | updateOrderStatus + upload | order-status-update; delivery-file-added/file |
ready_for_delivery | delivered | Admin/Editor | updateOrderStatus | — (stamps delivered_at) |
delivered | in_progress | Customer | request_revision | revision-requested (editor) |
| (revision) | sub-stage | Admin/Editor | set_revision_stage | revision-progress (cust + editor) |
| (assignment) | unchanged | Auto (submit) / Admin (override) | auto_assign_order / assign_order | in-app order_assigned (auto); editor-assignment email (manual) |
Audit: every status change is auto-logged to order_status_history by
the log_order_status_change trigger (from/to/by); notes attach to the freshly logged row.
Credit & Billing Journey
▾Credits are a unit-less internal currency. The balance is
server-authoritative — a column guard raises if a non-admin tries to change
credit_balance.
- Currency: credit packs are priced in USD, and new orders now stamp
currency='USD'too (the earlier CAD default was fixed; already-charged rows keep the currency they were charged in). Edit orders recordamount_cents=0— they're paid in credits, not dollars — and credits are redeemable across all edit types. - Pricing source of truth:
package_prices(slug → base_credits, clip_limit) andaddon_prices(slug → credits, is_rush). Overage = 5 credits/extra clip. The client sends slugs; the RPC never trusts client-supplied amounts. - Refund path: the only automated refund is
cancel_order, and only whileawaiting_footage— it restorescredits_spentand writes anorder_cancelledtransaction. No partial/post-work refund and no in-app Stripe-dollar refund (a dollar refund is done manually in Stripe; credits are the in-app currency).
Edge Cases & Gaps
▾auto_assign_order, flag on in app_settings) to the
least-loaded active editor under capacity, with an in-app notification. The remaining edge is
saturation: if every active editor is at max_concurrent_orders, auto-assign
leaves the order awaiting_footage and unassigned until an admin reassigns or an editor
frees up — there's no queue-overflow alert yet, so the admin should watch the unassigned filter on
the orders board.
| Scenario | What happens today | State |
|---|---|---|
| Failed / abandoned payment | No webhook → no credits, no order. Return page now verifies real session status (old version showed a false green check). | fixed |
| Underpayment via tampered price | Was possible (regex on a client string). Now costed server-side from price tables; unknown slugs rejected. | fixed |
| RLS-denied access | Query returns empty/denied — but most routes lack isError handling, so it hangs on "Loading…" or shows a false "No edits yet." | open |
| New order needs an editor | Auto-assigned at submit to the least-loaded active editor under capacity (auto_assign_order). No admin action needed on the happy path. | works |
| All editors at capacity | Auto-assign finds no eligible editor and leaves the order awaiting_footage/unassigned until an editor frees up or an admin reassigns. No overflow alert yet. | watch |
| Revision after delivery | Supported & safe — the one customer-driven status change RLS whitelists. Editor notified; sub-stages tracked. | works |
| Uninvited visitor tries to sign up | Access is controlled by the invite-only waitlist (the public CTA points to /waitlist), not an in-app invite code. /signup itself has no code gate; gating is operational (who gets invited off the waitlist). | by design |
| New customer with no credits | Starter credits = 0, so a fresh account has an empty balance and submit_edit_order raises "Insufficient credits" until they buy. Comped testers are topped up via adminAdjustCredits. | by design |
Cancel after awaiting_footage | Customers can only self-cancel while still awaiting footage. Once work starts, an admin can cancel & refund from the order workspace (refunds credits, emails both parties). | works |
| No-credit submit | submit_edit_order raises "Insufficient credits" before creating the order; UI also disables submit. | works |
| Over-capacity editor (manual assign) | Manual assign_order still assigns but returns over_capacity:true as a soft warning. The capacity count now includes accepted/uploading_footage (under-count fixed), so auto-assign's hard cap and this warning reflect true load. | fixed |
| Admin in editor dropdown | Admins can appear in the dropdown; assigning one then errors. Filter to role='editor'. | open |
| Editor availability write | Was silently failing (trigger + REVOKE). Fixed via the set_editor_availability SECURITY DEFINER RPC. | fixed |
| Duplicate status emails | Server fn and admin UI both send on status change. Remove the client-side send. | open |
| Unauthenticated cron hook | editor-due-soon now requires a Bearer secret (constant-time compare). Was open to anyone. | fixed |
| Brand inconsistency | Auth emails branded "summit-edit-flow"; contact email domain updated to info@summitediting.co to match sender notify.summitediting.co. Auth email branding still needs alignment. | open |
| Illegal status jumps | No transition guard — admin/editor can set any status (e.g. skip stages, delivered→in_progress outside revision). | post-launch |
Touchpoint Matrix
▾Each journey step → route/screen → server function or RPC → email fired.
Customer
| Step | Route / Screen | Server fn / RPC / Trigger | |
|---|---|---|---|
| Discover | /, /pricing, /how-it-works, /ai-examples, /faq | — | — |
| Join waitlist | /waitlist | Fillout embed (WAITLIST_URL) | — |
| Sign up (invited) | /signup | signUp → handle_new_user (grants starter_credits=0) | signup (verify) |
| Verify email | link → /dashboard | Supabase Auth + lovable/email/auth/webhook | (verify link) |
| Buy credits | /buy-credits | StripeEmbeddedCheckout | — |
| Payment confirm | /checkout/return | checkStatus; payments/webhook | — |
| Submit edit | /submit | submit_edit_order RPC | — |
| Track status | /dashboard, /dashboard/orders/:id | order select (RLS); StatusGauge | order-status-update |
| Receive delivery | /dashboard/orders/:id/delivery | order_deliveries; signed URL | delivery-file-added |
| Download | (same) | logs order_delivery_downloads | — |
| Request revision | /dashboard/orders/:id | requestRevision → request_revision | revision-requested (editor) |
| Revision progress | /dashboard/orders/:id | (editor's set_revision_stage) | revision-progress |
| Cancel (early only) | /dashboard/orders/:id | cancelOrder → cancel_order | order-cancelled |
| Contact | /contact | api/public/contact (+Gmail relay) | contact-confirmation |
Editor
| Step | Route / Screen | Server fn / RPC | Email (received) |
|---|---|---|---|
| Invited | link → /login | inviteEditor → inviteUserByEmail | Supabase invite |
| Set availability | /editor/availability | setEditorAvailability → set_editor_availability | — |
| Get assigned | (in-app notification) | auto_assign_order (at submit) / assign_order (admin override) | editor-assignment (manual only) |
| View queue | /editor/index | getEditorQueue | — |
| Work order | /editor/orders/:id → /admin/orders/:id | updateOrderStatus (RLS: assigned only) | — |
| Upload delivery | /admin/orders/:id | upload → order_deliveries; notifyDeliveryUploaded | — |
| Mark ready/delivered | /admin/orders/:id | updateOrderStatus | — |
| Revision work | /admin/orders/:id | setRevisionStage → set_revision_stage | revision-requested, revision-progress |
| Due-soon alert | (cron) | api/public/hooks/editor-due-soon | editor-due-soon |
| Order cancelled | (notification) | cancel_order (customer) | order-cancelled-editor |
Admin
| Step | Route / Screen | Server fn / RPC | |
|---|---|---|---|
| Orders board | /admin/index | order select (full access) | — |
| Inbox | /admin/inbox | inbox.functions (Gmail) | — |
| Reassign order (override) | /admin/orders/:id | updateOrderAssignment → assign_order (auto-assign already ran at submit) | editor-assignment |
| Set due date | /admin/orders/:id | updateOrderDueDate | — |
| Change status | /admin/orders/:id | updateOrderStatus | order-status-update |
| Internal notes | /admin/orders/:id | saveAdminNotes (order_internal_notes) | — |
| Cancel & refund | /admin/orders/:id | cancelOrder → cancel_order | order-cancelled (+ editor) |
| Manage editors | /admin/editors | inviteEditor, removeEditorRole, editor_workload_summary() | (invite) |
| Manage customers | /admin/customers, /admin/customers/:id | getAdminCustomerDetail, adminAdjustCredits (grant/deduct) | — |
| Reconcile payment | /admin/reconcile | adminReconcileSession (credit a missed Stripe session) | — |
| Due-soon console | /admin/due-soon-emails | due-soon-emails.functions | — |