Skip to content

shivang-goliyan/Strata-Pay

Repository files navigation

StrataPay

Multi-tenant billing platform with hybrid data isolation, deterministic proration, and a full enterprise dashboard — built from scratch with NestJS, Next.js 14, PostgreSQL, and Stripe.

Landing Page


Why This Exists

Every SaaS billing tool I've used gets the hard stuff wrong. They slap WHERE tenant_id = ? on queries and call it "multi-tenancy." They delegate proration to Stripe's black box and hope the numbers match. They treat webhook delivery as fire-and-forget.

StrataPay is the platform I wanted to exist:

  • Real tenant isolation — not just a column filter. Small tenants share a PostgreSQL schema with RLS enforcement. Enterprise tenants get their own schema, switched at the connection level via search_path. Same ORM, same pool, different isolation guarantees.
  • Proration you can verify — every cent is computed with decimal.js (ROUND_HALF_UP), recorded in a PlanTransition audit trail, then sent to Stripe as a fixed amount. The preview in the UI matches the invoice. Always.
  • Webhook delivery that doesn't drop events — exponential backoff, dead letter queue, per-endpoint delivery logs with one-click retry.

Screenshots

Enterprise Dashboard

Tenant Dashboard — Subscription status, quick links, real-time stats with trend indicators

Super Admin — Tenant Management

Super Admin Portal — Manage tenants, isolation modes, filter by status

API Keys Management

API Keys — Scoped permissions, usage tracking, security scoring

Authentication

Auth — JWT + OAuth (Google, GitHub), role-based access control


Architecture

┌──────────────────────────────────────────────────────────┐
│                    Next.js 14 (App Router)                │
│   ┌──────────┐  ┌──────────────┐  ┌──────────────────┐  │
│   │  Admin UI │  │ Tenant Dash  │  │   Auth (Login)   │  │
│   └──────────┘  └──────────────┘  └──────────────────┘  │
└───────────────────────┬──────────────────────────────────┘
                        │ HTTP / JWT
┌───────────────────────▼──────────────────────────────────┐
│                     NestJS API                            │
│   ┌────────┐ ┌──────────┐ ┌─────────┐ ┌──────────────┐  │
│   │  Auth  │ │ Tenancy  │ │ Billing │ │  Webhooks    │  │
│   │  RBAC  │ │ Context  │ │ Prorate │ │  Audit       │  │
│   └────────┘ └────┬─────┘ └────┬────┘ └──────────────┘  │
│                   │            │                          │
│            ┌──────▼────────────▼──────┐                  │
│            │  Prisma + AsyncLocalStore │                  │
│            │  (tenant-scoped $tx)      │                  │
│            └──────────┬───────────────┘                  │
└───────────────────────┼──────────────────────────────────┘
                        │
          ┌─────────────┼─────────────┐
          ▼             ▼             ▼
   ┌────────────┐ ┌──────────┐ ┌──────────┐
   │ PostgreSQL │ │  Redis   │ │  Stripe  │
   │ 16 + RLS   │ │  7       │ │  API     │
   │            │ │ BullMQ   │ │          │
   └────────────┘ └──────────┘ └──────────┘

Tech Stack

Layer Technology Rationale
Backend NestJS + TypeScript (strict) Decorators + DI make multi-tenancy middleware clean
ORM Prisma Type-safe queries & migrations, raw SQL only for RLS policies
Database PostgreSQL 16 RLS, schema namespacing, search_path — the multi-tenancy trifecta
Queue Redis 7 + BullMQ Webhook retries, event dedup, tenant cache
Payments Stripe Subscription lifecycle, but we own the proration math
Frontend Next.js 14 + shadcn/ui + Tailwind App Router route groups, server components where possible
Infra Docker Compose + GitHub Actions CI Single command to spin up the full stack

Core Features

Multi-Tenant Data Isolation

Two isolation modes running side-by-side on one PostgreSQL instance:

  • Shared (RLS) — tenants share tables, isolated by row-level security policies. Zero overhead per tenant.
  • Dedicated (Schema) — enterprise tenants get their own Postgres schema. Tenant context set via SET LOCAL search_path at connection level — not query params, not middleware hacks.
  • Live migration — promote any tenant from shared → dedicated without downtime. Background job copies data, swaps the routing entry, verifies row counts.

Deterministic Proration Engine

  • All money math uses decimal.js with explicit rounding — no IEEE 754 floats in the money path
  • Credit (unused days on old plan) and charge (remaining days on new plan) rounded independently
  • Every plan change writes a PlanTransition record with the full formula breakdown
  • UI preview shows the exact amount before the user confirms
  • Stripe is told "charge this exact amount" — we don't use their proration

Webhook System

  • Tenant-scoped endpoint registration with secret signing keys
  • Event delivery with exponential backoff (1m → 2m → 4m → 8m → dead letter)
  • Full delivery log per endpoint with HTTP status codes and response times
  • One-click retry from the dashboard for failed/dead-letter deliveries

Audit Trail

  • Every create/update/delete is captured by a NestJS interceptor
  • Stores actor, entity, old values, new values, IP, user agent
  • Expandable diff view in the dashboard with dark-themed JSON blocks
  • Filterable by action type, entity type, and date range

API Key Management

  • Scoped permissions (billing:read, billing:write, team:read, etc.)
  • Key prefix + last four visible, full key shown once on creation
  • Expiration dates, usage tracking, security scoring
  • One-click revocation with immediate effect

Quick Start

git clone <repo-url> && cd stratapay
npm install

# start infrastructure
docker-compose up -d postgres redis

# setup database
cp .env.example .env
npx prisma migrate dev --schema=apps/api/prisma/schema.prisma
npx prisma db seed --schema=apps/api/prisma/schema.prisma

# run both apps
npm run dev:api   # → localhost:4000
npm run dev:web   # → localhost:3000

Or just docker-compose up for everything at once.


Demo Walkthrough

As Super Admin (admin@stratapay.dev)

  1. Log in → Admin portal with dark sidebar, tenant management view
  2. Browse all tenants — filter by status, isolation mode, search by name/slug
  3. Click into a tenant → see subscription details, user list, isolation upgrade option
  4. Check Metrics → MRR trend chart, plan distribution, new vs churned tenants

As Tenant Admin (cto@globex.com — Enterprise plan)

  1. Log in → Dashboard with subscription card, quick links, usage stats
  2. Billing → Current plan, invoices with status badges, payment method management
  3. Team → Invite members, change roles, remove users
  4. API Keys → Create scoped keys, track usage, revoke instantly
  5. Webhooks → Register endpoints, view delivery logs, retry failed deliveries
  6. Audit Log → Expandable entries with JSON diffs, metadata, filters

As Free Tier (founder@coolstartup.io)

  1. Same dashboard, usage bar shows current consumption against plan limits
  2. "Upgrade Plan" CTA visible in sidebar and billing page

All demo accounts use password: password123


Project Structure

stratapay/
├── apps/
│   ├── api/                 # NestJS backend
│   │   ├── src/
│   │   │   ├── auth/        # JWT, RBAC, guards
│   │   │   ├── tenancy/     # Schema router, RLS, context propagation
│   │   │   ├── billing/     # Proration engine, Stripe integration
│   │   │   ├── webhooks/    # Endpoint mgmt, delivery, dead letter queue
│   │   │   └── audit/       # Interceptor-based logging
│   │   └── prisma/          # Schema, migrations, seed
│   └── web/                 # Next.js 14 frontend
│       └── src/
│           ├── app/
│           │   ├── (auth)/       # Login, registration
│           │   ├── (dashboard)/  # Tenant-facing pages
│           │   └── (admin)/      # Super admin portal
│           ├── components/ui/    # shadcn/ui primitives
│           └── lib/              # API client, auth hooks, utils
├── packages/
│   └── shared/              # Cross-app types, constants, validation
├── docs/                    # Screenshots
├── docker-compose.yml
└── .github/workflows/ci.yml

Design Decisions

Decision What I chose Why
Tenant isolation search_path at connection level Impossible to forget isolation in a new method — it's baked into the connection
Proration Own engine over Stripe's Stripe's proration is a black box with edge cases around trials and rounding. I needed the UI preview to match the charge exactly
Transaction atomicity Stripe-first, then DB If payment fails, nothing changes. Reverse (DB-first) risks showing a plan nobody's paying for
Context propagation AsyncLocalStorage Tenant context set once in middleware, available everywhere in the call chain without passing it through 15 function signatures
Frontend routing Next.js route groups (auth), (dashboard), (admin) each get their own layout without URL nesting

Seed Accounts

Email Role Tenant Plan
admin@stratapay.dev Super Admin Platform
admin@acme.com Tenant Admin Acme Corp Starter ($29/mo)
dev@acme.com Member Acme Corp
cto@globex.com Tenant Admin Globex Corp Enterprise ($299/mo, dedicated)
founder@coolstartup.io Tenant Admin Cool Startup Free

Known Issues / Roadmap

  • DDL propagation to dedicated schemas is manual — need to hook into Prisma's migration lifecycle or use pg_cron
  • Expired ProcessedStripeEvent rows accumulate — need a cleanup cron
  • No email transport wired up yet — magic link tokens log to console in dev
  • Webhook delivery table lacks tenantId column — dedicated schemas sidestep this but shared schema needs it
  • Rate limiting on API key endpoints not implemented yet

License

MIT

About

No description, website, or topics provided.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages