Files
HOA_Financial_Platform/ONBOARDING-AND-AUTH.md
olsch01 a996208cb8 feat: add annual billing, free trial, upgrade/downgrade, and ACH invoice support
- 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>
2026-03-18 08:04:51 -04:00

22 KiB

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
  2. Stripe Billing & Checkout
  3. 14-Day Free Trial
  4. Monthly / Annual Billing
  5. Provisioning Pipeline
  6. Account Activation (Magic Link)
  7. Guided Onboarding Checklist
  8. Subscription Management & Upgrade/Downgrade
  9. ACH / Invoice Billing
  10. Access Control & Grace Periods
  11. Authentication & Sessions
  12. Multi-Factor Authentication (TOTP)
  13. Single Sign-On (SSO)
  14. Passkeys (WebAuthn)
  15. Environment Variables Reference
  16. Manual Intervention & Ops Tasks
  17. What's Stubbed vs. Production-Ready
  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)

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:

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

{ "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)

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