A production-ready multi-tenant SaaS backend framework built with Java 21 & Spring Boot 3.
https://saas-platform-engine-production.up.railway.app
Demo credentials:
- Email:
admin@demo.com - Password:
Demo1234!
Every SaaS product needs the same plumbing before it can ship its first feature: tenant isolation, authentication, roles, billing, audit trails, feature flags and usage limits. SaaS Platform Engine is that plumbing, done properly β a fork-and-go Spring Boot backend where multi-tenancy is enforced at the query level, permissions are declarative annotations, and metering/auditing happen off the request thread via AOP. It's built for backend developers who want to start a SaaS from a foundation that already behaves like a production system, not a tutorial.
ββββββββββββββββββββββββββββββββββββββββββββββββββ
β Spring Boot App β
HTTP Request β β
βββββββββββββββΆ TenantFilter βββΆβ JwtAuthFilter βββΆ Controller βββΆ Service βββΆ JPAββββΆ PostgreSQL
(header / resolves β verifies RS256 @Requires- @Transactionalβ (row-level
subdomain) tenant into β JWT, overrides Permission β tenant_id
ThreadLocal β tenant from claim @TrackUsage β isolation)
β @FeatureFlagRequired β
β β β
β βββββββββββββββΌββββββββββββββ β
β βΌ βΌ βΌ β
β AuditService MeteringService FlagCache ββΌβββΆ Redis
β (async pool) (async pool + β (flags:* cache,
β β @Scheduled agg.) β refresh:* tokens)
β βΌ βΌ β
β audit_logs usage_records/summaries β
ββββββββββββββββββββββββββββββββββββββββββββββββββ
- Multi-tenancy β row-level isolation (
tenant_idon every table), tenant resolved fromX-Tenant-IDheader, JWT claim or subdomain; pluggableTenancyStrategy(shared-schema / schema-per-tenant) - Auth β RS256 JWT access tokens (15 min) + single-use rotating refresh tokens in Redis (7 days)
- RBAC β system roles (OWNER/ADMIN/MEMBER/SUPER_ADMIN), tenant-scoped custom roles (PRO+), declarative
@RequiresPermission("billing:manage")enforcement via AOP - Billing β plans, subscriptions, invoices, Stripe checkout/portal/webhooks with signature verification; automatic mock mode without credentials
- Audit log β every action captured asynchronously with before/after snapshots, IP and user agent; paginated, filterable API
- Feature flags β per-tenant flags with percentage rollout, Redis cache (60s TTL) with write-through invalidation,
@FeatureFlagRequiredgate that 404s disabled endpoints - Usage metering β
@TrackUsageAOP: synchronous plan-limit check (429 on breach), async usage recording, per-minute aggregation job - Onboarding β one transaction creates tenant + owner + FREE subscription and returns tokens immediately
- Super admin β cross-tenant management: list, inspect usage, override plan, suspend
- Demo dashboard β single-file dark SPA with live usage rings, flag toggles, billing and a built-in API explorer
- Ops β Flyway migrations, Testcontainers test suite, multi-stage Docker build, GitHub Actions CI, Railway deployment
1. Row-level multi-tenancy over schema-per-tenant (default).
Shared schema with a tenant_id column keeps operations cheap: one connection pool, one migration run, trivial cross-tenant analytics for the platform operator. The trade-off is that isolation lives in code, so the rule here is structural: repositories only expose finder methods that require a tenantId parameter, and the tenant always comes from the verified JWT β a spoofed X-Tenant-ID header can never switch an authenticated caller's tenant. SeparateSchemaTenancy exists behind the same TenancyStrategy interface for customers who contractually need physical separation.
2. RS256 (asymmetric) JWT instead of HS256. With HS256 every service that verifies tokens can also mint them β one leaked shared secret compromises the platform. With RS256 the private key never leaves the auth component; API gateways, sidecar services or a future microservice split verify with the public key only. For a SaaS designed to grow past one deployable, asymmetric is the correct default even while the app is still a monolith.
3. AOP for audit, metering and permissions.
@RequiresPermission, @TrackUsage and @TrackAudit keep cross-cutting policy out of business logic: a controller method declares what it needs, aspects decide how it's enforced. Audit and metering writes are handed to dedicated executors β a slow audit insert can never add latency to a user request, and losing one audit row on crash is an accepted trade-off documented in code.
4. Redis cache for feature flags β bounded staleness, explicit invalidation.
Flag reads happen on hot paths, so they're served from Redis (flags:{tenant}:{key}, 60s TTL) instead of hitting PostgreSQL per request. Writes delete the cache key, so toggles apply immediately on the instance that made the change and within 60 seconds everywhere else β a deliberate consistency/performance trade-off. If Redis is down, evaluation falls back to the database: flags degrade in latency, not in correctness.
git clone https://github.com/addictcode/saas-platform-engine.git
cd saas-platform-engine
docker compose up --buildOpen http://localhost:8080 and click Try Demo (admin@demo.com / Demo1234!).
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /onboarding/start |
π | Create tenant + owner, returns tokens |
| GET | /onboarding/check-slug/{slug} |
π | Check slug availability |
| POST | /auth/register |
π | Join an existing tenant |
| POST | /auth/login |
π | Login, returns access + refresh tokens |
| POST | /auth/refresh |
π | Rotate refresh token |
| POST | /auth/logout |
β | Revoke refresh token |
| GET | /api/v1/tenant |
any | Current tenant info |
| PATCH | /api/v1/tenant |
settings:manage |
Update tenant settings |
| GET | /api/v1/tenant/usage |
billing:read |
Usage vs plan limits |
| GET | /api/v1/users |
users:read |
List users |
| POST | /api/v1/users/invite |
users:manage |
Invite user (seat-limited) |
| GET | /api/v1/users/me |
any | Current profile |
| PATCH | /api/v1/users/{id} |
users:manage |
Update user |
| DELETE | /api/v1/users/{id} |
users:manage |
Deactivate user |
| POST | /api/v1/users/{id}/roles |
users:manage |
Assign role |
| GET | /api/v1/roles |
users:read |
List roles |
| POST | /api/v1/roles |
users:manage |
Create custom role (PRO+) |
| POST | /api/v1/roles/{id}/permissions |
users:manage |
Set role permissions |
| GET | /api/v1/flags |
any | List feature flags |
| POST | /api/v1/flags |
flags:manage |
Create flag |
| PATCH | /api/v1/flags/{key} |
flags:manage |
Toggle / rollout % |
| DELETE | /api/v1/flags/{key} |
flags:manage |
Delete flag |
| GET | /api/v1/beta/dashboard |
any | Flag-gated example (404 while off) |
| GET | /api/v1/billing/subscription |
billing:read |
Current subscription |
| GET | /api/v1/billing/invoices |
billing:read |
Invoice history |
| POST | /api/v1/billing/checkout |
billing:manage |
Start upgrade checkout |
| POST | /api/v1/billing/portal |
billing:manage |
Billing portal link |
| GET | /api/v1/plans |
any | Plan catalogue |
| GET | /api/v1/usage |
any | Usage report |
| GET | /api/v1/data |
any | Metered demo endpoint (429 past limit) |
| GET | /api/v1/audit |
audit:read |
Audit log (paginated, filterable) |
| GET | /admin/tenants |
admin:super |
List all tenants |
| GET | /admin/tenants/{id} |
admin:super |
Tenant details + usage |
| PATCH | /admin/tenants/{id}/plan |
admin:super |
Override plan |
| POST | /admin/tenants/{id}/suspend |
admin:super |
Suspend tenant |
| POST | /webhook/stripe |
signature | Stripe webhook |
Create your own tenant:
curl -X POST http://localhost:8080/onboarding/start \
-H 'Content-Type: application/json' \
-d '{"companyName":"Acme Inc","email":"you@acme.dev","password":"Sup3rSecret!","fullName":"Jane Doe"}'Login and call the API:
TOKEN=$(curl -s -X POST http://localhost:8080/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"admin@demo.com","password":"Demo1234!"}' | jq -r .accessToken)
curl http://localhost:8080/api/v1/tenant/usage -H "Authorization: Bearer $TOKEN"Toggle a feature flag (takes effect immediately):
curl -X PATCH http://localhost:8080/api/v1/flags/beta_dashboard \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"enabled":true,"rolloutPct":25}'com.saasengine
βββ config/ Security, CORS, Redis, JPA, async executors, Stripe gateway selection
βββ multitenancy/ TenantContext (ThreadLocal), TenantFilter, interceptor, isolation strategies
βββ auth/ JWT (RS256) issuing/verification, refresh token rotation, login/register
βββ onboarding/ Tenant provisioning, slug generation
βββ domain/
β βββ tenant/ Tenant aggregate
β βββ user/ Users, invitations, seat limits
β βββ rbac/ Roles, permissions, grants
β βββ subscription/ Plans and subscriptions
β βββ billing/ Invoices, checkout, Stripe gateway (real + mock)
β βββ audit/ Async audit trail
β βββ featureflags/ Flags with Redis cache + rollout
β βββ metering/ Usage records, limits, aggregation job
βββ api/ REST controllers (v1)
βββ admin/ Cross-tenant super admin API
βββ shared/ Exceptions, @RequiresPermission/@TrackUsage/@TrackAudit + aspects
| Variable | Default | Description |
|---|---|---|
SPRING_DATASOURCE_URL |
jdbc:postgresql://localhost:5432/saasengine |
PostgreSQL JDBC URL |
SPRING_DATASOURCE_USERNAME / _PASSWORD |
saasengine |
Database credentials |
SPRING_DATA_REDIS_HOST / _PORT |
localhost:6379 |
Redis (or REDIS_URL in prod profile) |
SPRING_PROFILES_ACTIVE |
β | docker / prod |
STRIPE_API_KEY |
empty | Empty β billing mock mode |
STRIPE_WEBHOOK_SECRET |
empty | Webhook signature verification |
ALLOWED_ORIGINS |
* |
CORS allowlist (prod profile) |
APP_JWT_ACCESS_TOKEN_EXPIRY |
900 |
Access token TTL, seconds |
APP_JWT_REFRESH_TOKEN_EXPIRY |
604800 |
Refresh token TTL, seconds |
APP_TENANCY_STRATEGY |
shared-schema |
shared-schema / separate-schema |
./mvnw test # requires Docker (Testcontainers spins up PostgreSQL + Redis)| Suite | Covers |
|---|---|
OnboardingIntegrationTest |
Tenant provisioning, duplicate email β 409, slug availability, validation β 422 |
MultiTenancyIntegrationTest |
Cross-tenant isolation, spoofed X-Tenant-ID header is ignored, 401 without token |
RbacIntegrationTest |
MEMBER blocked (403), OWNER allowed, ADMIN grant unlocks abilities, super admin gate |
MeteringIntegrationTest |
Usage recording, FREE limit β 429, upgrade lifts limit |
FeatureFlagIntegrationTest |
CRUD, 404-gating, immediate cache invalidation on toggle |
JwtServiceTest |
Claims round-trip, expiry, wrong key, tampering |
FeatureFlagServiceTest |
Cache hit skips DB, invalidation on update, deterministic rollout buckets |
SlugServiceTest |
Slugification, collision suffixing |
- Email verification & password reset flows
- OAuth2 social login (Google/GitHub) per tenant
- Outbound webhook system with retries and HMAC signing
- Usage analytics dashboard (time-series rollups)
- Terraform provider for tenant provisioning
MIT