> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cfosilvia.com/llms.txt
> Use this file to discover all available pages before exploring further.

# DEMO ACCOUNT ACCESS

# One-time demo account sign-in links

For staff and trusted external viewers of the real shared demo account.
Operators create, list and revoke invitations with a small command-line utility.
There is no admin UI, reusable login credential, or change to Clerk session settings.

## Setup

1. Apply `20260916_01_demo_access_invitations.sql` through the normal migration
   pipeline before creating invitations on the target deployment.
2. Set `DEMO_ACCOUNT_CLERK_ID` on that deployment to the **real Clerk user ID**
   of the shared demo account in the same Clerk instance as the deployment.
   This is not `user_demo_silvia_portfolio` (the separate read-only sample portfolio).
   The account must already have completed onboarding and contain the intended demo data.
3. The deployment also uses its existing `CLERK_SECRET_KEY`,
   `SUPABASE_SERVICE_ROLE_KEY` and `NEXT_PUBLIC_SUPABASE_URL`.
   Generate a dedicated secret with `openssl rand -hex 32` and set it as
   `DEMO_ACCESS_OPERATOR_TOKEN` on the deployment and in the local operator file.
   For this preview, scope the Vercel variable to `feature/demo-sign-in-links`
   and redeploy after saving it. Use a different value for production.
4. Confirm the target Clerk instance already has a seven-day maximum session
   lifetime. This implementation does not change that setting or any inactivity timeout.
5. Create a local, ignored `.env.demo-staging.local` file for the operator utility:

   ```dotenv theme={null}
   DEMO_ACCESS_BASE_URL=https://your-staging-deployment.example
   DEMO_ACCESS_OPERATOR_TOKEN=your-staging-demo-operator-token
   # Only if the deployment requires Vercel automation access:
   # VERCEL_AUTOMATION_BYPASS_SECRET=your-existing-bypass-secret
   ```

Use environment-specific credentials. The base URL determines which deployment
creates the link. Never put the operator token or Vercel bypass secret into the
viewer’s link. A protected preview still requires the viewer’s normal deployment
access; use the established staging URL for external testing.

The operator secret authorizes create/list/revoke for the configured demo account.
It is never sent to viewers. To rotate it, replace the deployment value, redeploy,
and update the local operator file. Rotation does not revoke existing invitations
or viewer sessions; revoke those explicitly when needed.

## Operator workflow

```sh theme={null}
npx tsx --env-file=.env.demo-staging.local scripts/demo-access.ts create "Designer — September"
npx tsx --env-file=.env.demo-staging.local scripts/demo-access.ts list
npx tsx --env-file=.env.demo-staging.local scripts/demo-access.ts revoke <invitation-id>
```

`create` returns the invitation ID, label, link, and deadline for first use.
Copy the link and share it manually. It is returned only on creation; `list`
never returns login credentials. No email or SMS is sent by this workflow.

`list` shows invitation and Clerk session status, first use, last session
activity, and actual session expiration. `link_expired` describes an unused
link; it does not mean an already-created session has expired.

`revoke` invalidates an unused Clerk ticket or revokes the invitation’s recorded
session. Other viewers remain signed in. A `503` means revocation was not
confirmed: retry the same ID after the Clerk/database service recovers. Until confirmed, `list` reports
`revocation_pending`, not successful revocation. Repeating a completed revoke
is safe. Do not ban or delete the shared Clerk user to revoke one viewer.

A `409` means the recorded session belongs to a different account, or the
configured demo user cannot be found in the deployment's Clerk instance. Retrying
alone cannot fix it. Verify that the deployment's Clerk keys and
`DEMO_ACCOUNT_CLERK_ID` belong to the intended environment, then inspect the
invitation's `clerk_user_id` and `session_id` linkage. Do not revoke the foreign
session. If necessary, identify the viewer's actual session under the configured
demo account using Clerk's backend tools and revoke that verified session
manually; correct the linkage before retrying the invitation. Persistent `503`s
also require checking configuration and server diagnostics, not endless retries.

A Clerk `404` for the ticket or recorded session completes revocation because
that resource no longer exists, but only after verifying the configured demo
user exists in that Clerk instance. A missing user requires manual investigation
(`409`); other lookup failures remain retryable (`503`). Clerk's specific
`400 sign_in_token_cannot_be_revoked_code` also completes ticket revocation:
the ticket is no longer pending and cannot be redeemed again. Neither response
discovers any unrecorded sessions from interrupted sign-ins. If the ticket was
consumed without a recorded session, inspect the demo account's Clerk sessions
and revoke the viewer's verified session manually (see below).

## Viewer workflow and lifetime

* Open the full invitation and click **Open demo**. Merely fetching or
  previewing the link does not redeem it.
* If already signed into Silvia, open the original invitation in a private
  window or another browser. The entry page does not replace an existing session.
* The unused link expires after **24 hours** and can be redeemed once.
* After sign-in, return directly to `/dashboard` in the same browser. The
  resulting ordinary Clerk session follows the instance settings: **seven days
  when configured as above**, possibly less if revoked, signed out, inactive
  under the instance policy, or cookies are cleared.
* After expiry/sign-out, or to use another browser, ask the operator for a new link.
* If sign-in cannot complete, ask for a replacement. Refreshing the cleaned
  entry URL does not restore the invitation credentials; reopen the original link.

## Implementation

* Internal API: `GET/POST /api/internal/demo-access`,
  `DELETE /api/internal/demo-access/<id>`, protected by `authorizeDemoOperator()`.
  Only `DEMO_ACCESS_OPERATOR_TOKEN` is accepted; there is no fallback to the
  shared `INTERNAL_API_TOKEN`.
  Exact middleware exclusions preserve the internal Authorization header.
* Public entry: `/demo-access#code=...`. The fragment contains the invitation
  ID and ordinary Clerk ticket. Client instrumentation removes it before
  telemetry initializes; it is kept only in memory for redemption.
* Authenticated completion: `POST /api/demo-access/complete` verifies the
  Clerk identity, session and ticket hash, then atomically associates one session
  with the invitation. The server never accepts a caller-supplied session ID.
* Invitation rows are server-only with RLS enabled and no client policies.
  Only the ticket hash and Clerk token ID are stored, never the raw ticket.
* Revocation racing with completion prevents binding and ends the new session.
  A failed persistence operation also attempts to end the new session.

This is for trusted viewers sharing full account access. It does not isolate
chat history or edits, block account settings, or add a per-request access gate.
Session revocation follows Clerk’s normal propagation behavior. A browser closed
between Clerk sign-in and completion can leave an unrecorded session: operators
can inspect the demo account’s sessions using Clerk’s backend tools if needed.
Do not promise protection against a viewer deliberately bypassing completion.

## Staging acceptance check

1. Create two labeled invitations and redeem them in separate browser profiles.
   Both should reach the existing demo dashboard, with no onboarding or plan changes.
2. List invitations. Each should have its own session ID, `usedAt`, `active`
   status, and session expiration matching the staging Clerk settings.
3. Close and reopen a browser and visit `/dashboard`; it should reuse the
   ordinary session without the invitation.
4. Revoke the first invitation. After Clerk refreshes, its browser should be
   signed out while the second browser continues working.
5. Revoke a third invitation before first use; its link must fail to sign in.
6. Retry a consumed link in another browser; it must fail and require a new link.
7. Check that the entry URL loses its fragment before analytics/replay captures
   it, and that operator list output contains no raw ticket.

The migration and `DEMO_ACCOUNT_CLERK_ID` are staging prerequisites. Unit tests
use mocks; they do not prove a deployed Clerk/Supabase configuration works.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.