evo.platform
The part every SaaS rebuilds, built once.
Tenancy, authentication, RBAC, billing, email and audit logging as a service the products call — not a library each one forks. Three products run on it today. The source is public under MIT.
The problem it solves
Every multi-tenant product needs the same unglamorous half: workspaces, sign-up, verified email, password rules, sessions, refresh tokens, roles, per-app access, subscriptions, dunning, invitations, an audit trail. None of it is what the product is for, and all of it has to be right before the product can be sold.
The usual outcome is that it gets rebuilt per app, slightly differently each time — which means a fix to a token bug lands in one product and not the other two, and nobody can say which. evo.platform exists so there is one implementation, and the products are tenants of it.
It is deliberately not a framework you inherit from. An app talks to it over HTTP through an SDK, keeps its own database and its own business logic, and can be developed with the platform switched off entirely.
What it is
- A platform service — NestJS, Prisma and PostgreSQL, with an admin console for tenants, users, per-app access, revenue, audit, SMTP and the email copy itself.
- Two SDKs — Node and Python, so an app in either language talks to the same API without hand-rolling token handling.
- A Next.js starter template that runs standalone by default: a local users table, one implicit tenant, no external dependency. Set one environment variable and the same code is fully multi-tenant.
- A scaffold CLI —
evo new <app>produces a tenant-aware application.
Flow 1 — signing up
Self-service sign-up arrives through an app, never at the platform directly. One request creates the workspace, its founding admin, and trial access to exactly the app the person came through — and nobody can sign in until the mailbox is proven.
- One call does it.
POST /auth/signupcarries the app'sclientId. The password is checked against the NIST/OWASP policy here, at the moment it is set — never at login, so tightening the rule cannot lock out an existing account. - Trial access is scoped to the app you arrived through. Every other app stays denied by default rather than enabled by oversight.
- The mailbox is proven before anything works. Until the link is clicked, login is refused with Email not verified.
- The platform initiates exactly one thing in this whole flow — the email. It never calls the app, and it never calls the browser.
Flow 2 — a tenant site, request by request
The everyday path: browser, edge, app, with login delegated to the platform and everything after it verified locally. The blue steps are the design's point.
- Four gates, in order — workspace status, credentials, verified mailbox, then per-app enablement. No enablement row means refused; there is no implicit yes.
- Pull, not push. The app fetches the platform's public keys once and caches them, then verifies every later request by signature, locally. After login, the app does not need the platform to be up.
- Tenant data never leaves the app. The platform holds identity and billing; business data lives in the app's own database, always tenant-scoped.
- Billing changes bite at a boundary. A Stripe webhook flips the enablement row, and it takes effect at the next login or token refresh — not in the middle of somebody's request.
Flow 3 — an app asking evo-ai
Ask AI as EvoCivilCode runs it. evo-ai is a separate service; this is how an app
built on the platform consumes it through the SDK's AskAi client, which
is optional.
- The tenant comes from the session, not the request. A caller cannot name somebody else's workspace.
- Isolation is by construction — a search runs inside the tenant's own
collection, filtered by
tenant_id, rather than being filtered after the fact. - The relevance gate refuses before a model is paid for. A refusal costs nothing and cannot hallucinate.
- Refusals come back as designed states, flagged
gatedorunconfigured, so the app renders them deliberately instead of showing an error.
Four decisions that shaped it
- Standalone by default. Every template runs with no platform at all — local users, one implicit tenant, zero external dependencies. You do not have to adopt the platform to use the template, which means the template gets tried on its own merits.
- The platform never calls an app. Every arrow points at the platform, except the one email it sends. That is what lets an app keep serving verified requests while the platform is restarting.
- Identity and billing here; business data there. The split is the whole reason a product can be sold without the platform becoming a single point of data loss.
- Offline-first in the browser. The Next.js template ships an IndexedDB mutation queue with last-write-wins sync, so a tenant app survives a bad connection rather than blocking on one.
In production
It is not a demo. Three products run on it as tenants today — evo.provensheet, evo.civilcode and evo.docketmail — which is also what keeps it honest: a breaking change to the platform is a breaking change to three live applications, so the API is treated as a published contract rather than an internal detail.
The source is public and MIT licensed, including the admin console, both SDKs, the starter template and the scaffold CLI: github.com/evomedia-net/evo.platform.
Technology stack
- Service — NestJS, Node.js, Prisma, PostgreSQL with row-level tenant isolation
- Identity — JWT signed RS256 with rotating refresh tokens, JWKS for public-key distribution, WebAuthn passkeys
- Billing — Stripe subscriptions and webhooks
- Clients — Node and Python SDKs, a Next.js starter template, and the
evo newscaffold CLI - Delivery — Docker, with the admin console shipped alongside the service