Skip to main content

Release the customer portal

Use this gate for any production change that depends on the customer collaboration schema, portal Edge Function, Trigger schedules, or customer email. Keep the email gate disabled until one controlled recipient completes the acceptance flow.

Confirm the production target​

Verify these targets before any release check:

SurfaceProduction target
Vercel appcprinos-7741s-projects/sable
Vercel rootweb
App URLhttps://sable.indigotrigger.ai
Supabase projectfvngkfrmienkltuwtoxa
Collaboration migration20260911000449_customer_portal_collaboration.sql
Resource activation migration20260911024049_portal_resource_activation.sql
Unified release migration20260911024549_unify_deliverable_release_approval.sql

Production releases follow the Git path in Deployment. Do not run a direct Vercel production deploy, Supabase function deploy, Trigger deploy, or workstation db push.

Run the pre-merge checks​

Run the implementation suites from the repository root:

node --test supabase/tests/*.test.mjs web/tests/*.test.mjs
cd <sable-checkout> && npm test
npx tsc --noEmit --moduleResolution bundler --module esnext --target es2022 --types node --skipLibCheck trigger.config.ts trigger/*.ts trigger/lib/*.ts
cd docs-site && npm run build

Confirm each migration executes against an isolated production-equivalent database before approving the release. The portal migrations are additive, emit no customer notification backfill, and give existing members in-app notifications with email disabled.

Apply schema before dependent workers​

The collaboration route, Edge Function, and Trigger worker require the migration’s tables and remote procedure calls (RPCs). Complete these checks in order:

  1. Apply the reviewed migration through .github/workflows/sable-migrate.yml.
  2. Verify the remote migration ledger contains 20260911000449, 20260911024049, and 20260911024549 when the release includes resource activation and unified approval release.
  3. Merge the checked application release.
  4. Wait for .github/workflows/sable-deploy.yml to deploy Edge Functions and the Trigger worker.
  5. Verify Vercel generated production from the merged main commit.

If schema and application changes share one release, keep SABLE_PORTAL_EMAIL_DELIVERY_ENABLED false until the migration and both worker deployments succeed. Portal API routes fail closed when their required tables or configuration are unavailable.

Client Portal Essentials deploy order​

The Client Portal Essentials release (customer tabs, This week, important links, auto-share, weekly drafts) adds portal_policies, portal_links, the auto_release_client_meetings function and an inbox-email backfill. Deploy in this order:

  1. Migrations, through sable-migrate.yml. Run the backfill (20260923180000_backfill_inbox_email_channel.sql) in a low-traffic window: it updates existing email sources rows in place.
  2. Edge functions, so portal-notifications knows about the tab guard and inbound mail records forwarded senders.
  3. Vercel, from the merged main commit.
  4. Trigger, so the four new tasks register.

The new reads tolerate missing tables: until the migration lands, every client reads as the default five tabs with no links, and the Trigger tasks log once and do nothing. Saving tab or automation settings still fails until the tables exist.

Configure access email separately from activity email:

  1. Set the Supabase Auth Site URL to the production SABLE origin.
  2. Add /auth-callback.html URLs for production and approved acceptance-test previews to the redirect allowlist.
  3. Configure and verify Supabase Auth SMTP for invitations and password recovery.
  4. Set SABLE_WEB_URL=https://sable.indigotrigger.ai in the Vercel and Supabase Edge environments.

Invitation and recovery links must return to the exact authorized client, view, and id destination. The callback rejects cross-origin and protocol-relative redirect targets.

Configure the notification transport​

Set these Supabase Edge secrets by name:

  • RESEND_API_KEY
  • RESEND_FROM
  • CRON_SECRET
  • SABLE_WEB_URL
  • SABLE_PORTAL_EMAIL_DELIVERY_ENABLED

The Trigger environment needs SUPABASE_URL, SUPABASE_ANON_KEY, and the same CRON_SECRET. The worker passes the anon key to the Supabase gateway and x-cron-secret to the Edge Function’s scheduled-caller gate.

Start with SABLE_PORTAL_EMAIL_DELIVERY_ENABLED=false. The in-app outbox and inbox continue to operate while activity email stays gated.

Verify delivery readiness without sending email​

Call the health mode through scheduled-caller authentication:

curl -sS "$SUPABASE_URL/functions/v1/portal-notifications" \
-H "Authorization: Bearer $SUPABASE_ANON_KEY" \
-H "apikey: $SUPABASE_ANON_KEY" \
-H "x-cron-secret: $CRON_SECRET" \
-H "content-type: application/json" \
--data '{"mode":"health"}'

Health mode does not claim work or contact a recipient. It reports whether the gate is enabled, required Resend secrets exist, the provider responds, and the sender domain can send. readiness.ready becomes true only when the email gate and verified sender domain are both ready.

Verify every Trigger schedule​

Confirm the deployed Trigger worker registered these tasks:

TaskSchedule
sable.portal.notifications.dispatchEvery 2 minutes
sable.portal.notifications.digest.daily08:15 America/New_York daily
sable.portal.notifications.digest.weekly08:30 America/New_York every Monday
sable.portal.notifications.digest.retryEvery 15 minutes
sable.portal.notifications.reminders09:05 America/New_York on weekdays
sable.portal.attachments.cleanup03:35 America/New_York daily
sable.portal.notifications.healthNot scheduled; run by hand
sable.weekly-update.draftFriday 12:00 America/New_York
sable.weekly-update.draft.projectChild run, one per project, started by the task above
sable.portal.auto-releaseHourly
sable.portal.auto-release.clientOn demand with { clientId }, started when a lead turns automatic sharing on

The four automation tasks never approve or publish a draft, run on their own queues, and do not call the Edge function. The six notification tasks share the sable-portal-notifications queue with concurrency one, as does the on-demand health task. Each of those retries the Edge invocation up to five times. Email delivery rows have their own retry and lease state inside Supabase.

Before flipping a delivery gate, run sable.portal.notifications.health. It returns the Edge function's readiness object — gate_enabled, whether the Resend key and sender are configured, provider_reachable and sender_domain_verified — and sends nothing: it claims no outbox rows and contacts no recipient. Run it again afterwards; gate_enabled should be the only field that changed.

Run controlled acceptance​

Use synthetic records and one approved recipient. Do not create customer test data or send unsolicited customer messages.

Verify these boundaries:

  • Anonymous requests receive 401 from every portal API
  • A Client A member cannot read or mutate Client B records
  • A revoked member receives 403 with the existing Auth token
  • Invitation acceptance and recovery return to the exact safe portal destination
  • A client_viewer can comment and reply but cannot approve
  • A client_reviewer can decide the current released version
  • A consultant sees only active or current assigned projects
  • Resources and Ask SABLE contain released evidence only
  • Publication inventory totals match available, shared, and release-ready records
  • A consultant can browse only assigned active or current projects and cannot publish
  • An admin or lead can publish and withdraw curated resource metadata with revision protection
  • Direct customer update publication creates one update and one idempotent receipt
  • Draft deep links reject a draft outside the authorized project instead of opening another record
  • Direct Workbench release rejects missing source drafts, non-ready quality gates, QA issues, and missing inputs
  • Exact release replay and unchanged-current-content replay create no duplicate version or email
  • A legacy deliverable_release approval either creates the same immutable release or remains pending on a readiness 409
  • Batch approval refuses deliverable_release rows and reports that individual release is required
  • Internal notes stay out of customer payloads, search, email, and assistant evidence
  • A stale revision returns 409 and retrying one idempotency key does not duplicate work
  • A real supported file completes signed upload, publishes with a reply, and downloads through a 60-second signed URL
  • Request assignment, next-response ownership, resolution receipt, milestone changes, and review decisions persist after reload
  • Customer tabs & automation: turning a tab off hides it from the customer (staff still see it, marked hidden), the landing tab must be one that is on, and a stale save returns 409
  • This week shows the latest update, recent shared meetings and, when set, important links and key documents; it appears only when the lead enables it
  • Important links can be added, reordered and archived by a lead, reject non-http(s) addresses, and a project-scoped link is hidden from members outside that project
  • Automatic meeting sharing releases only client-domain meetings with a title and summary; the recording link stays unshared unless its separate setting is on (it is off by default), and the transcript is never shared
  • With weekly drafts on, the Friday run creates a draft in Drafts that stays pending_approval; nothing reaches the customer until a person approves it
  • Approving a publishing item that is no longer eligible (Internal source, or a weekly wrap whose listed meeting has passed or changed) stays pending and returns 409 with a Regenerate message

Keep activity email disabled during the first pass. Confirm in-app notification materialization, unread state, and preferences before testing transport.

Activate a customer workspace​

Activation uses real customer-facing records, never seeded production activity:

  1. Open the customer’s staff preparation view.
  2. Review exact counts for memberships, shared resources, published updates, deliverables, milestones, and requests.
  3. Browse document, meeting, and draft candidates within active or current projects.
  4. Publish approved resource metadata with a customer-facing title and summary.
  5. Publish a direct update only when a named staff member approves that customer communication.
  6. Release approved deliverables from Draft Workbench.
  7. Publish the shared plan milestone and commitments.
  8. Preview each customer tab the client has enabled, including This week if it is on (staff see every other disabled tab marked hidden, except This week, which staff also see only when it is on), and run a released-evidence Ask SABLE question.
  9. Invite the named customer and record the factual delivery state.

The publication preview must remain metadata-only. Do not copy a document body or meeting transcript into a customer summary. Withdrawing an explicit share requires the current revision and a reason; the document remains visible when an active immutable released deliverable still links it.

Enable activity email with one recipient​

After health and in-app checks pass:

  1. Set SABLE_PORTAL_EMAIL_DELIVERY_ENABLED=true in the Supabase Edge environment.
  2. Repeat health mode and require readiness.ready: true.
  3. Enable activity email for the controlled recipient.
  4. Create one explicit request or review event.
  5. Confirm the inbox row, delivery row, Resend provider ID, and exact portal link.
  6. Test one daily or weekly digest in the recipient’s saved time zone.
  7. Revoke or disable the synthetic recipient after acceptance.

Do not infer transport success from an outbox row. A successful path has a materialized inbox row and a portal_notification_deliveries.state of delivered. pending, processing, retry, suppressed, and failed are distinct operational outcomes.

Verify lease recovery and cleanup​

The worker uses 180-second delivery leases and stable provider idempotency keys. Verify an expired processing lease can be reclaimed and that a persisted digest batch completes or defers as one group.

Attachment cleanup processes unpublished drafts after 24 hours. Confirm the cleanup task moves a claimed row to deleting, removes its private Storage object, and records deleted. A failed deletion must become cleanup_failed with a future retry, and an expired deleting lease must remain reclaimable.