Skip to main content

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:
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

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 503s 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.