- Add monthly/annual billing toggle with 25% annual discount on pricing page - Implement 14-day no-card free trial (server-side Stripe subscription creation) - Enable upgrade/downgrade via Stripe Customer Portal - Add admin-initiated ACH/invoice billing for enterprise customers - Add billing card to Settings page with plan info and Manage Billing button - Handle past_due status with read-only grace period access - Add trial ending and trial expired email templates - Add DB migration for billing_interval and collection_method columns - Update ONBOARDING-AND-AUTH.md documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
588 lines
22 KiB
Markdown
588 lines
22 KiB
Markdown
# HOA LedgerIQ -- Payment, Onboarding & Authentication Guide
|
|
|
|
> **Version:** 2026.03.18
|
|
> **Last updated:** March 18, 2026
|
|
> **Migrations:** `db/migrations/015-saas-onboarding-auth.sql`, `db/migrations/017-billing-enhancements.sql`
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [High-Level Flow](#1-high-level-flow)
|
|
2. [Stripe Billing & Checkout](#2-stripe-billing--checkout)
|
|
3. [14-Day Free Trial](#3-14-day-free-trial)
|
|
4. [Monthly / Annual Billing](#4-monthly--annual-billing)
|
|
5. [Provisioning Pipeline](#5-provisioning-pipeline)
|
|
6. [Account Activation (Magic Link)](#6-account-activation-magic-link)
|
|
7. [Guided Onboarding Checklist](#7-guided-onboarding-checklist)
|
|
8. [Subscription Management & Upgrade/Downgrade](#8-subscription-management--upgradedowngrade)
|
|
9. [ACH / Invoice Billing](#9-ach--invoice-billing)
|
|
10. [Access Control & Grace Periods](#10-access-control--grace-periods)
|
|
11. [Authentication & Sessions](#11-authentication--sessions)
|
|
12. [Multi-Factor Authentication (TOTP)](#12-multi-factor-authentication-totp)
|
|
13. [Single Sign-On (SSO)](#13-single-sign-on-sso)
|
|
14. [Passkeys (WebAuthn)](#14-passkeys-webauthn)
|
|
15. [Environment Variables Reference](#15-environment-variables-reference)
|
|
16. [Manual Intervention & Ops Tasks](#16-manual-intervention--ops-tasks)
|
|
17. [What's Stubbed vs. Production-Ready](#17-whats-stubbed-vs-production-ready)
|
|
18. [API Endpoint Reference](#18-api-endpoint-reference)
|
|
|
|
---
|
|
|
|
## 1. High-Level Flow
|
|
|
|
```
|
|
Visitor hits /pricing
|
|
|
|
|
v
|
|
Selects plan (Starter / Professional / Enterprise)
|
|
Chooses billing frequency (Monthly / Annual — 25% discount)
|
|
Enters email + business name
|
|
|
|
|
v
|
|
POST /api/billing/start-trial (no card required)
|
|
|
|
|
v
|
|
Backend creates Stripe customer + subscription with trial_period_days=14
|
|
Backend provisions: org -> schema -> user -> invite token -> email
|
|
|
|
|
v
|
|
Frontend navigates to /onboarding/pending?session_id=xxx
|
|
(polls GET /api/billing/status every 3s)
|
|
|
|
|
v
|
|
Status returns "active" -> user is redirected to /login
|
|
|
|
|
v
|
|
User clicks activation link from email
|
|
|
|
|
v
|
|
GET /activate?token=xxx -> validates token
|
|
POST /activate -> sets password + name, issues session
|
|
|
|
|
v
|
|
Redirect to /onboarding (4-step guided wizard)
|
|
|
|
|
v
|
|
Dashboard (14-day trial active)
|
|
|
|
|
v
|
|
Day 11: Stripe fires customer.subscription.trial_will_end webhook
|
|
Backend sends trial-ending reminder email
|
|
|
|
|
v
|
|
User adds payment method via Stripe Portal (Settings > Manage Billing)
|
|
|
|
|
v
|
|
Trial ends -> Stripe charges card -> subscription becomes 'active'
|
|
OR: No card -> subscription cancelled -> org archived
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Stripe Billing & Checkout
|
|
|
|
### Plans & Pricing
|
|
|
|
| Plan | Monthly | Annual (25% off) | Unit Limit |
|
|
|------|---------|-------------------|------------|
|
|
| Starter | $29/mo | $261/yr ($21.75/mo) | 50 units |
|
|
| Professional | $79/mo | $711/yr ($59.25/mo) | 200 units |
|
|
| Enterprise | Custom | Custom | Unlimited |
|
|
|
|
### Stripe Products & Prices
|
|
|
|
Each plan has **two Stripe Prices** (monthly and annual):
|
|
|
|
| Env Variable | Description |
|
|
|-------------|-------------|
|
|
| `STRIPE_STARTER_MONTHLY_PRICE_ID` | Starter monthly recurring price |
|
|
| `STRIPE_STARTER_ANNUAL_PRICE_ID` | Starter annual recurring price |
|
|
| `STRIPE_PROFESSIONAL_MONTHLY_PRICE_ID` | Professional monthly recurring price |
|
|
| `STRIPE_PROFESSIONAL_ANNUAL_PRICE_ID` | Professional annual recurring price |
|
|
| `STRIPE_ENTERPRISE_MONTHLY_PRICE_ID` | Enterprise monthly recurring price |
|
|
| `STRIPE_ENTERPRISE_ANNUAL_PRICE_ID` | Enterprise annual recurring price |
|
|
|
|
Backward compatibility: `STRIPE_STARTER_PRICE_ID` (old single var) maps to monthly if the new `_MONTHLY_` var is not set.
|
|
|
|
### Two Billing Paths
|
|
|
|
| Path | Audience | Payment | Trial |
|
|
|------|----------|---------|-------|
|
|
| **Path A: Self-serve (Card)** | Starter & Professional | Automatic card charge | 14-day no-card trial |
|
|
| **Path B: Invoice / ACH** | Enterprise (admin-set) | Invoice with Net-30 terms | Admin configures |
|
|
|
|
### Webhook Events Handled
|
|
|
|
| Event | Action |
|
|
|-------|--------|
|
|
| `checkout.session.completed` | Triggers full provisioning pipeline (card-required flow) |
|
|
| `invoice.payment_succeeded` | Sets org status to `active` (reactivation after trial/past_due) |
|
|
| `invoice.payment_failed` | Sets org to `past_due`, sends payment-failed email |
|
|
| `customer.subscription.deleted` | Sets org status to `archived` |
|
|
| `customer.subscription.trial_will_end` | Sends trial-ending reminder email (3 days before) |
|
|
| `customer.subscription.updated` | Syncs plan, interval, status, and collection_method to DB |
|
|
|
|
All webhook events are deduplicated via the `shared.stripe_events` table (idempotency by Stripe event ID).
|
|
|
|
---
|
|
|
|
## 3. 14-Day Free Trial
|
|
|
|
### How It Works
|
|
|
|
1. User visits `/pricing`, selects a plan and billing frequency
|
|
2. User enters email + business name (required)
|
|
3. Clicks "Start Free Trial"
|
|
4. Backend creates Stripe customer (no payment method)
|
|
5. Backend creates subscription with `trial_period_days: 14`
|
|
6. Backend provisions org with `status = 'trial'` immediately
|
|
7. User receives activation email, sets password, starts using the app
|
|
|
|
### Trial Configuration
|
|
|
|
| Setting | Description |
|
|
|---------|-------------|
|
|
| `REQUIRE_PAYMENT_METHOD_FOR_TRIAL` | `false` (default): no-card trial. `true`: uses Stripe Checkout (card required upfront). |
|
|
|
|
### Trial Lifecycle
|
|
|
|
| Day | Event |
|
|
|-----|-------|
|
|
| 0 | Trial starts, full access granted |
|
|
| 11 | `customer.subscription.trial_will_end` webhook fires |
|
|
| 11 | Trial-ending email sent ("Your trial ends in 3 days") |
|
|
| 14 | Trial ends |
|
|
| 14 | If card on file: Stripe charges, subscription becomes `active` |
|
|
| 14 | If no card: subscription cancelled, org set to `archived` |
|
|
|
|
### Trial Behavior by Plan Frequency
|
|
|
|
- **Monthly trial**: Trial ends, charge monthly price
|
|
- **Annual trial**: Trial ends, charge full annual amount
|
|
|
|
### Trial End Behavior
|
|
|
|
Configured in Stripe subscription: `trial_settings.end_behavior.missing_payment_method: 'cancel'`
|
|
|
|
When trial ends without a payment method, the subscription is cancelled and the org is archived. Users can resubscribe at any time.
|
|
|
|
---
|
|
|
|
## 4. Monthly / Annual Billing
|
|
|
|
### Pricing Page Toggle
|
|
|
|
The pricing page (`PricingPage.tsx`) features a segmented control toggle:
|
|
- **Monthly**: Shows monthly prices ($29/mo, $79/mo)
|
|
- **Annual (Save 25%)**: Shows effective monthly rate + annual total ($21.75/mo billed annually at $261/yr)
|
|
|
|
The selected billing frequency is passed to the backend when starting a trial or creating a checkout session.
|
|
|
|
### Annual Discount
|
|
|
|
Annual pricing = Monthly price x 12 x 0.75 (25% discount):
|
|
- Starter: $29 x 12 x 0.75 = **$261/yr**
|
|
- Professional: $79 x 12 x 0.75 = **$711/yr**
|
|
|
|
---
|
|
|
|
## 5. Provisioning Pipeline
|
|
|
|
When a trial starts or `checkout.session.completed` fires, the backend runs **inline provisioning**:
|
|
|
|
1. **Create organization** in `shared.organizations` with:
|
|
- `name` = business name from signup
|
|
- `schema_name` = `tenant_{random_12_chars}`
|
|
- `status` = `trial` (for trial) or `active` (for card checkout)
|
|
- `plan_level` = selected plan
|
|
- `billing_interval` = `month` or `year`
|
|
- `stripe_customer_id` + `stripe_subscription_id`
|
|
- `trial_ends_at` (if trial)
|
|
- Uses `ON CONFLICT (stripe_customer_id)` for idempotency
|
|
|
|
2. **Create tenant schema** via `TenantSchemaService.createTenantSchema()`
|
|
3. **Create or find user** in `shared.users` by email
|
|
4. **Create membership** in `shared.user_organizations` (role: `president`)
|
|
5. **Generate invite token** (JWT, 72-hour expiry)
|
|
6. **Send activation email** with link to set password
|
|
7. **Initialize onboarding** progress row
|
|
|
|
### Provisioning Status Polling
|
|
|
|
`GET /api/billing/status?session_id=xxx` (no auth required)
|
|
|
|
Accepts both Stripe checkout session IDs and subscription IDs. Returns: `{ status }` where status is:
|
|
- `not_configured` -- Stripe not set up
|
|
- `pending` -- no customer ID yet
|
|
- `provisioning` -- org exists but not ready
|
|
- `active` -- ready (includes `trial` status)
|
|
|
|
---
|
|
|
|
## 6. Account Activation (Magic Link)
|
|
|
|
### Validate Token
|
|
|
|
`GET /api/auth/activate?token=xxx` -- returns `{ valid, email, orgName, orgId, userId }`
|
|
|
|
### Activate Account
|
|
|
|
`POST /api/auth/activate` -- body `{ token, password, fullName }` -- sets password, issues session
|
|
|
|
---
|
|
|
|
## 7. Guided Onboarding Checklist
|
|
|
|
| Step Key | UI Label | Description |
|
|
|----------|----------|-------------|
|
|
| `profile` | Profile | Set up user profile |
|
|
| `workspace` | Workspace | Configure organization settings |
|
|
| `invite_member` | Invite Member | Invite at least one team member |
|
|
| `first_workflow` | First Account | Create the first chart-of-accounts entry |
|
|
|
|
---
|
|
|
|
## 8. Subscription Management & Upgrade/Downgrade
|
|
|
|
### Stripe Customer Portal
|
|
|
|
Users manage their subscription through the **Stripe Customer Portal**, accessed via:
|
|
- Settings page > Billing card > "Manage Billing" button
|
|
- Calls `POST /api/billing/portal` which creates a portal session and returns the URL
|
|
|
|
### What Users Can Do in the Portal
|
|
|
|
- **Switch plans**: Change between Starter and Professional
|
|
- **Switch billing frequency**: Monthly to Annual (and vice versa)
|
|
- **Update payment method**: Add/change credit card
|
|
- **Cancel subscription**: Cancels at end of current period
|
|
- **View invoices**: See billing history
|
|
|
|
### Upgrade/Downgrade Behavior
|
|
|
|
| Change | Behavior |
|
|
|--------|----------|
|
|
| Monthly to Annual | Immediate. Prorate remaining monthly time, start annual cycle now. |
|
|
| Annual to Monthly | Scheduled at end of current annual period. |
|
|
| Starter to Professional | Immediate. Prorate price difference. |
|
|
| Professional to Starter | Scheduled at end of current period. |
|
|
|
|
Stripe handles proration automatically when configured with `proration_behavior: 'create_prorations'`.
|
|
|
|
### Subscription Info Endpoint
|
|
|
|
`GET /api/billing/subscription` (auth required) returns:
|
|
```json
|
|
{
|
|
"plan": "professional",
|
|
"planName": "Professional",
|
|
"billingInterval": "month",
|
|
"status": "active",
|
|
"collectionMethod": "charge_automatically",
|
|
"trialEndsAt": null,
|
|
"currentPeriodEnd": "2026-04-18T00:00:00.000Z",
|
|
"cancelAtPeriodEnd": false
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 9. ACH / Invoice Billing
|
|
|
|
### Overview
|
|
|
|
For enterprise customers who need to pay via ACH bank transfer or wire, an admin can switch the subscription's collection method from automatic card charge to invoice billing.
|
|
|
|
### How It Works
|
|
|
|
1. **Admin** calls `PUT /api/admin/organizations/:id/billing` with:
|
|
```json
|
|
{ "collectionMethod": "send_invoice", "daysUntilDue": 30 }
|
|
```
|
|
2. Stripe subscription is updated: `collection_method = 'send_invoice'`, `days_until_due = 30`
|
|
3. At each billing cycle, Stripe generates an invoice and emails it to the customer
|
|
4. Customer pays via ACH / wire / bank transfer
|
|
5. When payment is received, Stripe marks invoice paid and org remains active
|
|
|
|
### Access Rules for Invoice Customers
|
|
|
|
| Stage | Access |
|
|
|-------|--------|
|
|
| Trial | Full |
|
|
| Invoice issued | Full |
|
|
| Due date passed | Read-only (past_due) |
|
|
| 15+ days overdue | Admin may archive |
|
|
|
|
### Switching Back
|
|
|
|
To switch back to automatic card billing:
|
|
```json
|
|
{ "collectionMethod": "charge_automatically" }
|
|
```
|
|
|
|
---
|
|
|
|
## 10. Access Control & Grace Periods
|
|
|
|
### Organization Status Access Rules
|
|
|
|
| Status | Access | Description |
|
|
|--------|--------|-------------|
|
|
| `trial` | **Full** | 14-day trial, all features available |
|
|
| `active` | **Full** | Paid subscription, all features available |
|
|
| `past_due` | **Read-only** | Payment failed or invoice overdue. Users can view data but cannot create/edit/delete. |
|
|
| `suspended` | **Blocked** | Admin suspended. 403 on all org-scoped endpoints. |
|
|
| `archived` | **Blocked** | Subscription cancelled. 403 on all org-scoped endpoints. Data preserved. |
|
|
|
|
### Implementation
|
|
|
|
- **Tenant Middleware** (`tenant.middleware.ts`): Blocks `suspended` and `archived` with 403. Sets `req.orgPastDue = true` for `past_due`.
|
|
- **WriteAccessGuard** (`write-access.guard.ts`): Blocks POST/PUT/PATCH/DELETE for `past_due` orgs with message: "Your subscription is past due. Please update your payment method."
|
|
|
|
---
|
|
|
|
## 11. Authentication & Sessions
|
|
|
|
### Token Architecture
|
|
|
|
| Token | Type | Lifetime | Storage |
|
|
|-------|------|----------|---------|
|
|
| Access token | JWT | 1 hour | Frontend Zustand store |
|
|
| Refresh token | Opaque (64 bytes) | 30 days | httpOnly cookie (`ledgeriq_rt`) |
|
|
| MFA challenge | JWT | 5 minutes | Frontend state |
|
|
| Invite/activation | JWT | 72 hours | URL query parameter |
|
|
|
|
### Session Endpoints
|
|
|
|
| Method | Path | Auth | Description |
|
|
|--------|------|------|-------------|
|
|
| `POST` | `/api/auth/login` | No | Email + password login |
|
|
| `POST` | `/api/auth/register` | No | Create account |
|
|
| `POST` | `/api/auth/refresh` | Cookie | Refresh access token |
|
|
| `POST` | `/api/auth/logout` | Cookie | Revoke current session |
|
|
| `POST` | `/api/auth/logout-everywhere` | JWT | Revoke all sessions |
|
|
| `POST` | `/api/auth/switch-org` | JWT | Switch organization |
|
|
|
|
---
|
|
|
|
## 12. Multi-Factor Authentication (TOTP)
|
|
|
|
### MFA Endpoints
|
|
|
|
| Method | Path | Auth | Description |
|
|
|--------|------|------|-------------|
|
|
| `POST` | `/api/auth/mfa/setup` | JWT | Generate QR code + secret |
|
|
| `POST` | `/api/auth/mfa/enable` | JWT | Enable MFA with TOTP code |
|
|
| `POST` | `/api/auth/mfa/verify` | mfaToken | Verify during login |
|
|
| `POST` | `/api/auth/mfa/disable` | JWT | Disable (requires password) |
|
|
| `GET` | `/api/auth/mfa/status` | JWT | Check MFA status |
|
|
|
|
---
|
|
|
|
## 13. Single Sign-On (SSO)
|
|
|
|
| Provider | Env Vars Required |
|
|
|----------|-------------------|
|
|
| Google | `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GOOGLE_CALLBACK_URL` |
|
|
| Microsoft/Azure AD | `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_TENANT_ID`, `AZURE_CALLBACK_URL` |
|
|
|
|
SSO providers are conditionally loaded based on env vars.
|
|
|
|
---
|
|
|
|
## 14. Passkeys (WebAuthn)
|
|
|
|
| Method | Path | Auth | Description |
|
|
|--------|------|------|-------------|
|
|
| `POST` | `/api/auth/passkeys/register-options` | JWT | Get registration options |
|
|
| `POST` | `/api/auth/passkeys/register` | JWT | Complete registration |
|
|
| `POST` | `/api/auth/passkeys/login-options` | No | Get authentication options |
|
|
| `POST` | `/api/auth/passkeys/login` | No | Authenticate with passkey |
|
|
| `GET` | `/api/auth/passkeys` | JWT | List user's passkeys |
|
|
| `DELETE` | `/api/auth/passkeys/:id` | JWT | Remove a passkey |
|
|
|
|
---
|
|
|
|
## 15. Environment Variables Reference
|
|
|
|
### Stripe (Required for billing)
|
|
|
|
| Variable | Description |
|
|
|----------|-------------|
|
|
| `STRIPE_SECRET_KEY` | Stripe secret key. Must NOT contain "placeholder" to activate. |
|
|
| `STRIPE_WEBHOOK_SECRET` | Webhook endpoint signing secret |
|
|
| `STRIPE_STARTER_MONTHLY_PRICE_ID` | Stripe Price ID for Starter monthly |
|
|
| `STRIPE_STARTER_ANNUAL_PRICE_ID` | Stripe Price ID for Starter annual |
|
|
| `STRIPE_PROFESSIONAL_MONTHLY_PRICE_ID` | Stripe Price ID for Professional monthly |
|
|
| `STRIPE_PROFESSIONAL_ANNUAL_PRICE_ID` | Stripe Price ID for Professional annual |
|
|
| `STRIPE_ENTERPRISE_MONTHLY_PRICE_ID` | Stripe Price ID for Enterprise monthly |
|
|
| `STRIPE_ENTERPRISE_ANNUAL_PRICE_ID` | Stripe Price ID for Enterprise annual |
|
|
|
|
Legacy single-price vars (`STRIPE_STARTER_PRICE_ID`, etc.) are still supported as fallback for monthly prices.
|
|
|
|
### Trial Configuration
|
|
|
|
| Variable | Default | Description |
|
|
|----------|---------|-------------|
|
|
| `REQUIRE_PAYMENT_METHOD_FOR_TRIAL` | `false` | Set to `true` to require card upfront via Stripe Checkout |
|
|
|
|
### SSO (Optional)
|
|
|
|
| Variable | Description |
|
|
|----------|-------------|
|
|
| `GOOGLE_CLIENT_ID` | Google OAuth client ID |
|
|
| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret |
|
|
| `GOOGLE_CALLBACK_URL` | OAuth redirect URI |
|
|
| `AZURE_CLIENT_ID` | Azure AD application (client) ID |
|
|
| `AZURE_CLIENT_SECRET` | Azure AD client secret |
|
|
| `AZURE_TENANT_ID` | Azure AD tenant ID |
|
|
| `AZURE_CALLBACK_URL` | OAuth redirect URI |
|
|
|
|
### WebAuthn / Passkeys
|
|
|
|
| Variable | Default | Description |
|
|
|----------|---------|-------------|
|
|
| `WEBAUTHN_RP_ID` | `localhost` | Relying party identifier |
|
|
| `WEBAUTHN_RP_ORIGIN` | `http://localhost` | Expected browser origin |
|
|
|
|
### Other
|
|
|
|
| Variable | Default | Description |
|
|
|----------|---------|-------------|
|
|
| `INVITE_TOKEN_SECRET` | `dev-invite-secret` | Secret for invite/activation JWTs |
|
|
| `APP_URL` | `http://localhost` | Base URL for generated links |
|
|
| `RESEND_API_KEY` | -- | Resend email provider API key |
|
|
|
|
---
|
|
|
|
## 16. Manual Intervention & Ops Tasks
|
|
|
|
### Stripe Dashboard Setup
|
|
|
|
1. **Create Products and Prices** for each plan:
|
|
- Starter: monthly ($29/mo recurring) + annual ($261/yr recurring)
|
|
- Professional: monthly ($79/mo recurring) + annual ($711/yr recurring)
|
|
- Enterprise: monthly + annual (custom pricing)
|
|
- Copy all Price IDs to env vars
|
|
|
|
2. **Configure Stripe Webhook** endpoint:
|
|
- URL: `https://yourdomain.com/api/webhooks/stripe`
|
|
- Events: `checkout.session.completed`, `invoice.payment_succeeded`, `invoice.payment_failed`, `customer.subscription.deleted`, `customer.subscription.trial_will_end`, `customer.subscription.updated`
|
|
|
|
3. **Configure Stripe Customer Portal**:
|
|
- Enable plan switching (allow switching between monthly and annual prices)
|
|
- Enable payment method updates
|
|
- Enable cancellation
|
|
- Enable invoice history
|
|
|
|
4. **Set production secrets**: `INVITE_TOKEN_SECRET`, `JWT_SECRET`, `WEBAUTHN_RP_ID`, `WEBAUTHN_RP_ORIGIN`
|
|
|
|
5. **Configure SSO providers** (optional)
|
|
|
|
### Ongoing Ops
|
|
|
|
- **Refresh token cleanup**: Schedule `RefreshTokenService.cleanupExpired()` periodically
|
|
- **Monitor `shared.email_log`**: Check for failed email deliveries
|
|
- **ACH/Invoice customers**: Admin sets up via `PUT /api/admin/organizations/:id/billing`
|
|
|
|
### Finding activation URLs (dev/testing)
|
|
|
|
```sql
|
|
SELECT to_email, metadata->>'activationUrl' AS url, sent_at
|
|
FROM shared.email_log
|
|
WHERE template = 'activation'
|
|
ORDER BY sent_at DESC
|
|
LIMIT 10;
|
|
```
|
|
|
|
---
|
|
|
|
## 17. What's Stubbed vs. Production-Ready
|
|
|
|
| Component | Status | Notes |
|
|
|-----------|--------|-------|
|
|
| Stripe Checkout (card-required flow) | **Ready** (test mode) | Switch to live keys for production |
|
|
| Stripe Trial (no-card flow) | **Ready** (test mode) | Creates customer + subscription server-side |
|
|
| Stripe Webhooks | **Ready** | All 6 events handled with idempotency |
|
|
| Stripe Customer Portal | **Ready** | Full org-context customer ID lookup implemented |
|
|
| Monthly/Annual Pricing | **Ready** | Toggle on pricing page, 6 Stripe Price IDs |
|
|
| ACH/Invoice Billing | **Ready** | Admin endpoint switches collection method |
|
|
| Provisioning | **Ready** | Inline, supports both trial and active status |
|
|
| Email service | **Ready** (with Resend) | Falls back to stub logging if not configured |
|
|
| Trial emails | **Ready** | Trial-ending and trial-expired templates |
|
|
| Access control (past_due) | **Ready** | Read-only grace period for failed payments |
|
|
| Activation (magic link) | **Ready** | Full end-to-end flow |
|
|
| Onboarding checklist | **Ready** | Server-side progress tracking |
|
|
| Refresh tokens | **Ready** | Needs scheduled cleanup |
|
|
| TOTP MFA | **Ready** | Full setup, enable, verify, recovery |
|
|
| SSO (Google/Azure) | **Ready** (needs keys) | Conditional loading |
|
|
| Passkeys (WebAuthn) | **Ready** | Registration, authentication, removal |
|
|
|
|
---
|
|
|
|
## 18. API Endpoint Reference
|
|
|
|
### Billing
|
|
|
|
| Method | Path | Auth | Description |
|
|
|--------|------|------|-------------|
|
|
| `POST` | `/api/billing/start-trial` | No | Start 14-day no-card trial |
|
|
| `POST` | `/api/billing/create-checkout-session` | No | Create Stripe Checkout (card-required flow) |
|
|
| `POST` | `/api/webhooks/stripe` | Stripe sig | Webhook receiver |
|
|
| `GET` | `/api/billing/status?session_id=` | No | Poll provisioning status |
|
|
| `GET` | `/api/billing/subscription` | JWT | Get current subscription info |
|
|
| `POST` | `/api/billing/portal` | JWT | Create Stripe Customer Portal session |
|
|
| `PUT` | `/api/admin/organizations/:id/billing` | JWT (superadmin) | Switch billing method (card/invoice) |
|
|
|
|
### Auth
|
|
|
|
| Method | Path | Auth | Description |
|
|
|--------|------|------|-------------|
|
|
| `POST` | `/api/auth/register` | No | Register new user |
|
|
| `POST` | `/api/auth/login` | No | Login (may return MFA challenge) |
|
|
| `POST` | `/api/auth/refresh` | Cookie | Refresh access token |
|
|
| `POST` | `/api/auth/logout` | Cookie | Logout current session |
|
|
| `POST` | `/api/auth/logout-everywhere` | JWT | Revoke all sessions |
|
|
| `GET` | `/api/auth/activate?token=` | No | Validate activation token |
|
|
| `POST` | `/api/auth/activate` | No | Set password + activate |
|
|
| `POST` | `/api/auth/resend-activation` | No | Resend activation email |
|
|
| `GET` | `/api/auth/profile` | JWT | Get user profile |
|
|
| `POST` | `/api/auth/switch-org` | JWT | Switch organization |
|
|
|
|
### Onboarding
|
|
|
|
| Method | Path | Auth | Description |
|
|
|--------|------|------|-------------|
|
|
| `GET` | `/api/onboarding/progress` | JWT | Get onboarding progress |
|
|
| `PATCH` | `/api/onboarding/progress` | JWT | Mark step complete |
|
|
|
|
---
|
|
|
|
## Database Tables & Columns
|
|
|
|
### Tables Added (Migration 015)
|
|
|
|
| Table | Purpose |
|
|
|-------|---------|
|
|
| `shared.refresh_tokens` | Hashed refresh tokens with expiry/revocation |
|
|
| `shared.stripe_events` | Idempotency ledger for Stripe webhooks |
|
|
| `shared.invite_tokens` | Activation/invite magic links |
|
|
| `shared.onboarding_progress` | Per-org onboarding step completion |
|
|
| `shared.user_passkeys` | WebAuthn credentials |
|
|
| `shared.email_log` | Email audit trail |
|
|
|
|
### Columns Added to `shared.organizations`
|
|
|
|
| Column | Type | Migration | Description |
|
|
|--------|------|-----------|-------------|
|
|
| `stripe_customer_id` | VARCHAR(255) UNIQUE | 015 | Stripe customer ID |
|
|
| `stripe_subscription_id` | VARCHAR(255) UNIQUE | 015 | Stripe subscription ID |
|
|
| `trial_ends_at` | TIMESTAMPTZ | 015 | Trial expiration date |
|
|
| `billing_interval` | VARCHAR(20) | 017 | `month` or `year` |
|
|
| `collection_method` | VARCHAR(20) | 017 | `charge_automatically` or `send_invoice` |
|
|
|
|
### Organization Status Values
|
|
|
|
`active`, `trial`, `past_due`, `suspended`, `archived`
|