Skip to content

Commit d042cb3

Browse files
feat(backend,web): require card at trial start via stripe checkout
* feat(backend,web): require card at trial start via stripe checkout Hosted Compass now starts signed-up users in awaiting_checkout, collects a card through Stripe Checkout with a 7-day trial at $8/month, and enforces read-only when payment is missing. Self-host stays fully writable when the stripe config block is omitted. The anonymous localStorage trial is unchanged. Co-authored-by: Tyler Dane <tyler-dane@users.noreply.github.com> * fix(web): keep anonymous trial chip on the local clock RootShell tests mock useAppAccess process-wide, which hid the sidebar countdown for never-signed-up visitors. Drive that chip from useTrialStatus again, and stop spying hasUserEverAuthenticated in useAppAccess tests. Co-authored-by: Tyler Dane <tyler-dane@users.noreply.github.com> * fix(backend): close cardless trial and repeat-trial holes Remove POST /api/billing/trial/start so hosted users cannot skip Checkout. Grant trial_period_days only on a first subscription, send live subscribers to the Billing Portal, apply same-second Stripe events, and default BACKFILL_CUTOFF to the far future so existing accounts are not grandfathered. Co-authored-by: Tyler Dane <tyler-dane@users.noreply.github.com> * refactor(web): derive app access from the anonymous trial hook useAppAccess now reuses useTrialStatus for the visitor clock instead of duplicating the localStorage effect and gate-exempt predicate. Co-authored-by: Tyler Dane <tyler-dane@users.noreply.github.com> * fix(backend,web): reuse portal for incomplete checkout An incomplete Stripe subscription stays awaiting_checkout with a subscription id; a second Checkout would open another subscription. Send those users to the Billing Portal, hide Manage billing until a customer exists, and idempotize the first Checkout session. Co-authored-by: Tyler Dane <tyler-dane@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Tyler Dane <tyler-dane@users.noreply.github.com>
1 parent 3109926 commit d042cb3

68 files changed

Lines changed: 2742 additions & 116 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/_deploy-environment.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ jobs:
9494
SUPERTOKENS_KEY: ${{ secrets.SUPERTOKENS_KEY }}
9595
SUPERTOKENS_POSTGRES_PASSWORD: ${{ secrets.SUPERTOKENS_POSTGRES_PASSWORD }}
9696
SUPERTOKENS_URI: ${{ secrets.SUPERTOKENS_URI }}
97+
STRIPE_SECRET_KEY: ${{ (inputs.environment == 'production' || inputs.environment == 'staging-cloud') && secrets.STRIPE_SECRET_KEY || '' }}
98+
STRIPE_WEBHOOK_SECRET: ${{ (inputs.environment == 'production' || inputs.environment == 'staging-cloud') && secrets.STRIPE_WEBHOOK_SECRET || '' }}
99+
STRIPE_PRICE_ID: ${{ (inputs.environment == 'production' || inputs.environment == 'staging-cloud') && secrets.STRIPE_PRICE_ID || '' }}
97100
run: |
98101
echo "Deploying Compass ${RELEASE_TAG} (image version: ${IMAGE_VERSION}) to ${{ inputs.environment }}"
99102
# Only "production" ever runs the production runtime.nodeEnv; every
@@ -181,6 +184,13 @@ jobs:
181184
" key: \"${POSTHOG_KEY}\"" \
182185
" host: \"${POSTHOG_HOST}\""
183186
fi
187+
if [ -n "$STRIPE_SECRET_KEY" ] && [ -n "$STRIPE_WEBHOOK_SECRET" ] && [ -n "$STRIPE_PRICE_ID" ]; then
188+
printf '%s\n' \
189+
'stripe:' \
190+
" secretKey: \"${STRIPE_SECRET_KEY}\"" \
191+
" webhookSecret: \"${STRIPE_WEBHOOK_SECRET}\"" \
192+
" priceId: \"${STRIPE_PRICE_ID}\""
193+
fi
184194
printf '%s\n' \
185195
'sync:' \
186196
" mongoUri: \"${SYNC_MONGO_URI}\"" \

bun.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

compass.example.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ supertokens:
4141
# key: REPLACE_WITH_POSTHOG_KEY
4242
# host: REPLACE_WITH_POSTHOG_HOST
4343

44+
# stripe:
45+
# secretKey: REPLACE_WITH_STRIPE_SECRET_KEY # restricted rk_test_... for staging
46+
# webhookSecret: REPLACE_WITH_STRIPE_WEBHOOK_SECRET
47+
# priceId: REPLACE_WITH_STRIPE_PRICE_ID
48+
# Omit the whole block for self-host. All three values are required together.
49+
4450
# Compass Sync service — required. The backend exits at startup without
4551
# serviceUrl/internalAuthToken, and self-host runs Sync by default. mongoUri
4652
# MUST point at an isolated database/user that cannot read the backend's

docs/Config/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,6 @@ database and must not share the backend's database user/data.
9898
|---|---|---|
9999
| `posthog.key` | No | PostHog project key injected into the web bundle. |
100100
| `posthog.host` | No | PostHog host injected into the web bundle. |
101+
| `stripe.secretKey` | No | Stripe secret (restricted `rk_test_...` on staging). All three Stripe keys are required together; omit the whole `stripe:` block on self-host. |
102+
| `stripe.webhookSecret` | No | Stripe webhook signing secret. |
103+
| `stripe.priceId` | No | Stripe Price id for the $8/month plan. |

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Internal documentation for engineers and agents working in the Compass repo.
1818
- Breakpoints, sidebar collapse, or layout at different viewport sizes: [Responsive Layout](./frontend/responsive-layout.md)
1919
- Local-first or storage behavior: [Offline Storage And Migrations](./features/offline-storage-and-migrations.md)
2020
- Backend routes and API behavior: [Backend Route Map](./backend/README.md), [Backend Request Flow](./backend/backend-request-flow.md), [Backend Error Handling](./backend/backend-error-handling.md)
21+
- Trial, pricing, or Stripe: [Billing And Trial](./features/billing.md)
2122
- A new calendar integration or Google-sync behavior: [Google Sync And SSE Flow](./features/google-sync-and-sse-flow.md), the `packages/sync` domain code directly
2223

2324
## Architecture And Domain

docs/backend/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Backend routes are registered from
2525
| Config | `packages/backend/src/config/config.routes.config.ts` | Public runtime config used by the web app. |
2626
| Auth | `packages/backend/src/auth/auth.routes.config.ts` | Compass-owned auth helpers and authenticated Google connect. SuperTokens also mounts recipe routes under `/api`. |
2727
| User | `packages/backend/src/user/user.routes.config.ts` | Profile and metadata for the active session. |
28+
| Billing | `packages/backend/src/billing/billing.routes.config.ts` | Session status, Checkout, Billing Portal, and unauthenticated Stripe webhook. Self-host omits Stripe keys and stays fully writable. |
2829
| Events | `packages/backend/src/event/event.routes.config.ts` | Event CRUD and reorder/delete helpers. |
2930
| Event stream | `packages/backend/src/events/events.routes.config.ts` | Authenticated SSE stream at `GET /api/events/stream`. |
3031
| Calendars | `packages/backend/src/calendar/calendar.routes.config.ts` | Calendar list and selection routes. |

docs/development/cli.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Primary file:
1515
| Command | Implementation | Notes |
1616
| --- | --- | --- |
1717
| `bun run cli purge-user --email <address> [--apply] [--out report.json]` | `packages/scripts/src/commands/purge-user.ts` | Deletes one user's API, Sync, and SuperTokens data. Defaults to dry-run. |
18+
| `bun run cli backfill-billing [--apply] [--batch-size 500] [--cutoff ISO]` | `packages/scripts/src/commands/backfill-billing.ts` | Places existing accounts without billing status onto a 7-day trial. Defaults to dry-run. |
1819
| `bun run cli purge-corrupt-sync-events [--apply]` | `packages/scripts/src/commands/purge-corrupt-sync-events.ts` | Deletes invalid Sync event documents. Defaults to dry-run. |
1920
| `bun run cli refresh-connection-states [--apply]` | `packages/scripts/src/commands/refresh-connection-states.ts` | Re-derives Sync connection state. Defaults to dry-run. |
2021
| `bun run cli manage-failed-jobs <list\|clear\|requeue> …` | `packages/scripts/src/commands/manage-failed-jobs.ts` | Operator tooling for Sync jobs that exhausted the self-heal requeue budget. Defaults to dry-run; pass `--apply` to persist. |

docs/development/feature-file-map.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,14 @@ the full picture.
9797
- User metadata service: `packages/backend/src/user/services/user-metadata.service.ts`
9898
- Mobile waitlist gate (web-only external link): `packages/web/src/components/MobileGate/MobileGate.tsx`
9999

100+
## Billing And Trial
101+
102+
- Shared plan/price copy: `packages/core/src/constants/billing.constants.ts`
103+
- Anonymous trial clock: `packages/web/src/billing/trial.storage.ts`, `packages/web/src/billing/useTrialStatus.ts`
104+
- Server access + paid gate: `packages/web/src/billing/useAppAccess.ts`, `packages/web/src/billing/BillingGateModal.tsx`
105+
- Backend billing: `packages/backend/src/billing`
106+
- Overview: [Billing And Trial](../features/billing.md)
107+
100108
## Environment And Infra
101109

102110
- Backend config parsing: `packages/backend/src/common/constants/config.constants.ts`

docs/features/billing.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Billing And Trial
2+
3+
Hosted Compass uses Stripe Checkout (subscription mode, 7-day trial, $8/month)
4+
and the Stripe Billing Portal. There is no Stripe.js and no publishable key in
5+
the web bundle.
6+
7+
Self-host installs omit the `stripe:` config block. `/api/config` then reports
8+
`billing.isConfigured: false`, the web never shows a paid gate, and event
9+
writes stay open.
10+
11+
## What users see
12+
13+
- **Never signed up:** the existing 7-day anonymous `localStorage` trial and
14+
`TrialGateModal`. Unchanged.
15+
- **Signed up, no card yet:** `awaiting_checkout`, read-only, `BillingGateModal`
16+
with Subscribe (Stripe Checkout).
17+
- **Trialing / active / past_due:** writable. `past_due` also shows a banner.
18+
- **Expired / canceled:** read-only until they subscribe again. A later
19+
Checkout does not grant another trial.
20+
21+
There is no `POST /api/billing/trial/start`. A trial only begins through
22+
Stripe Checkout (`trial_period_days` on the first subscription).
23+
24+
Existing accounts are not grandfathered. `bun run cli backfill-billing` places
25+
rows without `billing.subscriptionStatus` onto a 7-day trial. The default
26+
`BACKFILL_CUTOFF` is far in the future so every such row is included; set it
27+
to a past instant to grandfather newer signups. Those rows have no Stripe
28+
subscription id, so they self-expire locally when `trialEndsAt` passes.
29+
30+
## Staging
31+
32+
Set `STRIPE_SECRET_KEY` (restricted `rk_test_...`), `STRIPE_WEBHOOK_SECRET`,
33+
and `STRIPE_PRICE_ID` on the `staging-cloud` GitHub Environment. Config-only
34+
deploys need `./compass restart`. Confirm `/api/config` shows
35+
`billing.isConfigured: true`.
36+
37+
`staging-selfhosted` must not get those secrets — it is the live regression
38+
that self-host stays writable.
39+
40+
Sales tax (`automatic_tax`) is a Stripe Dashboard decision, not a code one,
41+
and should be settled before production keys.
42+
43+
## Key files
44+
45+
- Plan/price copy: `packages/core/src/constants/billing.constants.ts`
46+
- Status derivation: `packages/backend/src/billing/services/billing.service.ts`
47+
- Checkout / portal: `packages/backend/src/billing/services/stripe.service.ts`
48+
- Webhook: `packages/backend/src/billing/services/billing.webhook.service.ts`
49+
- Write guard: `packages/backend/src/billing/billing.guard.ts`
50+
- Web access: `packages/web/src/billing/useAppAccess.ts`

docs/self-hosting/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Start with [Run Compass on a server](./server-guide.md). It walks through a smal
66

77
If you only want to run Compass on your own computer, use the normal local development flow with Bun instead of the self-host installer.
88

9+
Self-host does not require Stripe. Omit the `stripe:` block so billing stays off and every account remains writable. Hosted Compass uses a 7-day trial then $8/month; see [Billing And Trial](../features/billing.md).
10+
911
## Compass architecture
1012

1113
When you self-host Compass on a server, you get a stack of small services. Only the public website and API are reachable from your browser. The databases stay private inside Docker.

0 commit comments

Comments
 (0)