Convex backend for Wrapper.
This package owns the authenticated APIs used by the CLI and the web app: user authentication (Better Auth), session lifecycle, relay join tickets, onboarding state, and billing (Autumn). It is the single source of truth for who may attach to which session.
hostSessionsessionId,ownerUserId,shell,cwdport,hostPid,sharedrelayState,relayLastChangedAtstatus(activeorclosed)createdAt,updatedAt,lastHeartbeatAt,closedAt,closeReason
relayTickettokenHash(only the hash of the ticket is stored, never the token itself)sessionId,role(hostorviewer),userIdcreatedAt,expiresAt,usedAt(single-use enforcement)
onboardinguserIdcompletedProfile,connectedCli- leftover
sharedFirstSessionis optional and cleared on write status(in_progressorcompleted)source,sourceOther,teamSizecreatedAt,updatedAt,completedAt
rateLimit- fixed-window counters keyed by action, used to throttle unauthenticated endpoints such as device-code issuance
convex/session.ts:
open: create or re-open a host session (owner-only)heartbeat: update liveness and share/port state (owner-only)close: close a host session (owner-only)listActive: list active sessions for the authenticated ownerauthorizeAttach: allow attach only if the caller is the owner or the session is sharedsetShareCode: owner-only; start sharing and store the SHA-256 of the access code, or stop sharing and clear it (revoking outstanding access)markStaleIfTimedOut(internal): scheduler task that auto-closes stale active sessionssetRelayState: owner-only relay presence sync (offline,connecting,online,error)
convex/deviceAuth.ts (wrappers around the Better Auth device flow):
requestDeviceCode: issue a device and user code (rate limited per client and globally)pollDeviceToken: exchange a device code for a session token (globally rate limited; per-code pacing enforced by the Better Authinterval/slow_downcontract)getDeviceCodeInfo: globally rate-limited mutation that looks up a pending code so the web page can show what is being approvedapproveDeviceCode/denyDeviceCode: authenticated approve or deny actions. Approving a device also marks the CLI as connected for onboarding.
convex/relay.ts:
issueHostTicket: owner-only short-lived ticket for the host relay socket, gated by the Autumn sharing entitlementissueViewerTicket(action): short-lived ticket for a viewer socket. The owner is always allowed on their own devices. A non-owner must present the correct share code, and non-owner attempts are rate limited per user, per hashed target bucket, and globally to stop code guessing without exposing session existenceconsumeTicket: single-use consumption during the relay handshake, called by the relay itself without a user identitycleanupTicket(internal): scheduled cleanup of used and expired ticket rows
convex/onboarding.ts:
getState: current onboarding progress for the authenticated usercompleteStep: update one onboarding checklist stepcomplete: finalize onboarding and persist optional attribution data
convex/billing.ts:
getState: current Free or Pro plan, and whether the signed-in user can open Stripe's billing portal (a customer exists after Pro checkout or a later downgrade)createProCheckout: create a Stripe checkout URL for the Pro plan through Autumn, used by both the CLI upgrade hint and the web upgrade buttoncreateBillingPortal: open Stripe's customer portal for users who already have a billing customer
convex/auth.ts:
- Better Auth configuration (Convex adapter, device authorization, bearer token exchange, social providers). See the auth model below.
- Public handlers use
protectedQuery/protectedMutationfromconvex/lib/middleware.ts. Identity is required for every session lifecycle operation. - Errors are normalized through
convex/lib/errors.tsandconvex/lib/types.ts. - The
BETTER_AUTH_SECRETis required in production. In development a known local secret is used. If the secret is missing outside development, the server falls back to an ephemeral per-instance secret so it fails closed (existing sessions stop validating) rather than starting with a predictable key. - The CLI authenticates with the device authorization flow, stores a session
token, then exchanges that token for a short-lived Convex JWT through the
bearer()plugin before calling any Convex function. A long-running host re-mints the JWT from the stored session token before it expires, so hours-long sessions keep authenticating. Seeapps/cli/util/convex-client.ts. - Session liveness uses a heartbeat timeout. Missing heartbeats trigger scheduler
cleanup and close the session with
closeReason: "stale_timeout".
Access to a session is deliberately narrow:
- Not shared: owner only. Every lifecycle call (open, heartbeat, close, relay state, host ticket) rejects anyone who is not the owner.
- Shared: the owner runs
setShareCode, which marks the session shared and stores only the SHA-256 of a secret code the host generates. To join, a non-owner must callissueViewerTicketwith the matching code. Knowing the session id alone is not enough, and the owner can still join their own session from any device without a code. - Revocation: unsharing (or closing) clears the code hash, and
consumeTicketre-checks the shared state, so outstanding viewer tickets stop working the moment the owner unshares. - Anti-guessing: non-owner
issueViewerTicketcalls are rate limited in a separate committed mutation before access validation. This prevents Convex transaction rollback from erasing failed-attempt counters.
Unknown, unshared, missing-code, and wrong-code sessions return the same access denial so callers cannot use the API to discover another user's active session.
This is a capability model (possession of the code grants access), which suits pair-prompting on a shared session. Note that every viewer who joins shares control of the same shell (anyone connected can type). A per-join host approval step could be layered on later if watch-only viewers are needed.
- Tickets are random tokens. Only their hash is stored (
tokenHash), so a database read never reveals a usable credential. - Tickets are single-use (
usedAt) and short-lived (expiresAt). - On consumption the backend re-checks that the session still exists and is still shared, so unsharing immediately invalidates outstanding viewer tickets.
- Relay sharing is gated by the
can_share_relayfeature on the Pro plan. - Plans are defined as code in
autumn.config.tsand pushed withbunx atmn. - The entitlement check fails open on billing-provider errors so a billing outage cannot break the core sharing flow. This is a deliberate availability choice; it never grants access to another user's data.
- The gate can be disabled entirely with
WRAPPER_AUTUMN_RELAY_SHARE_FEATURE_IDset to an empty value.
Session timeouts:
WRAPPER_SESSION_STALE_AFTER_MS: heartbeat timeout window (default300000)WRAPPER_SESSION_STALE_GRACE_MS: scheduler grace window (default30000)
Total stale-close delay is
WRAPPER_SESSION_STALE_AFTER_MS + WRAPPER_SESSION_STALE_GRACE_MS.
Relay tickets:
WRAPPER_RELAY_HOST_TICKET_TTL_MS: host ticket TTL (default30000)WRAPPER_RELAY_VIEWER_TICKET_TTL_MS: viewer ticket TTL (default60000)WRAPPER_AUTUMN_RELAY_SHARE_FEATURE_ID: Autumn entitlement checked before issuing a host relay ticket (defaultcan_share_relay)
From the monorepo root:
bun run audit
bunx tsc --noEmit -p packages/backend/tsconfig.json
bunx oxlint packages/backend/convex
(cd packages/backend && bunx convex codegen)
bun run --cwd packages/backend betterAuth:generate-schema
bun test packages/backend/tests
bun run --cwd packages/backend test:integrationRun betterAuth:generate-schema only after changing the Better Auth schema
configuration; review and commit the generated schema update with the source
change.
wrapper auth login- Start a wrapped shell (
wrapper shell-hostor a normal wrapped terminal) wrapper attach --id <sessionId>- Detach the viewer (
Ctrl+\, thend) - Exit the host shell and verify the session closes
- Kill the host process without a clean close and verify the stale timeout auto-closes the session