[ AUTH & IDENTITY ]
Auth you don't have to build, on a database you already own.
Sessions, passkeys, two-factor, SSO, organisations and role-based permissions, as one module for Nuxt and Elysia. The user table lives in your PostgreSQL — we never hold a copy.
Closed beta already in production with selected partnersBuilt on better-auth extended, not forkedIncluded in Scale CHF 99 a month
We ship it before we sell it.
Every product we run signs its users in through this module — our own SaaS, the client platforms we operate, the back office behind this site. Not a proof of concept: the thing we fix ourselves when it breaks at two in the morning.
- Your database, not ours
- The module writes to your PostgreSQL. No JAMflow service holds a second copy of your users.
- One module, not five integrations
- Sessions, providers, organisations, invitations and permissions arrive together and already know about each other.
- The people who built it answer the ticket
- You hit an edge case and you are talking to the team that wrote the module, not to an integrator reading its README.
What ships, line by line.
The module is better-auth already wired, so what better-auth does, it does — with our permission layer and the operational pieces on top. Everything here arrives configured, except where the line says on request.
Ways in
- Email and password, with a reset that cuts every open session
- Magic links — one use, short-lived, stored hashed
- Passkeys over WebAuthn
- Second factor by one-time code
- Social and corporate sign-in over OIDC and OAuth2
Teams, roles and the record
- Organisations, members, and invitations that expire
- Roles per organisation, declared per resource and per action
- Platform roles that resolve above every tenant
- Support impersonation, from the platform tier
- An append-only audit log, hash-chained and verifiable
Isolation and limits
- One schema, scoping enforced by the framework
- A PostgreSQL schema per tenanton request
- A database of its ownon request
- Rate limiting on the auth routes, on Redis, shared across replicas
Authorisation
Roles that match your product, not a library's idea of one.
Your roles shouldn't live in a spreadsheet. A real product has owners, billing managers, support agents and read-only guests — and the same person is an owner in one organisation and a viewer in the next. You declare that once; the check is one call.
up to 50%
fewer database round-trips on a permission-checked request
A permission check normally costs a query: read the rule, then do the work. Here it comes off the session the request already carries: two round-trips become one. Arithmetic, not a benchmark — what it saves depends on what else that request reads.
- Platform roles resolve first
- Support and operations get their own tier above the tenants, so nobody is made an organisation admin in order to answer a ticket.
- One vocabulary, server and interface
- The same file is imported by the guard and by the interface, so a button and the route behind it cannot disagree about who may press it.
shared/permissions.ts
// One vocabulary, imported by the server and by the UIexport const org = createAccessControl({ ...defaultStatements, projects: ['read', 'update'], billing: ['read', 'update'], domains: ['read', 'create', 'update', 'delete'],}) export const member = org.newRole({ projects: ['read'], billing: ['read'],}) // A handler states both vocabularies; the platform one is tried firstawait requireResolvedPermission(event, { globalPermissions: { projects: ['update'] }, organizationPermissions: { projects: ['update'] },})One person, many organisations, no bleed.
The active organisation comes from the session, before your handler's logic runs, and a member who names a different one in the URL is refused, not quietly served something else. The framework holds that boundary in application code — a real one, not an uncrossable one.
- Organisations people manage themselves
- Create, join, switch. Nobody opens a ticket to move a person between two teams.
- Invitations that expire
- Signed, time-boxed, sent by email, with the role granted the moment the invitation is accepted.
- A session that already knows
- Who is signed in, for which organisation, and what they may do — on the first render, no loading state.
- A back-office tier
- Platform operators get user management and access across every organisation, apart from any tenant's roles.
One module, one migration, and the schema is yours.
The module registers its own composables, server utilities, middleware and routes, and brings its own Drizzle migration set. Point it at a PostgreSQL connection string, run the migration once, and the identity tables are in your database, under your migrations.
bun i @jamflow/authprivate registry — access comes with the plan
The rest of the toolkit is JAMlabs → Runs on JAMflow Postgres, Neon, or your own instance →
Data and access
The user table is yours.
The schema is under Drizzle migrations, in your repository, and you can read it. There is nothing to export out of, because nothing of yours sits with us — and the record of what people did in there is append-only and hash-chained.
Sending this to a security team? Data residency, certification and deployment model are on the enterprise page, and anything not written there we answer by name. If procurement needs a certificate in our own name today, read the first line of this page again. How we answer a security review →
Questions before you commit.
The ones we actually get asked. Anything else, ask us directly — a person answers.
Ask us something else →Is this just better-auth with a bill attached?
No. better-auth is the authentication foundation; everything above it — the permission layer, the platform tier, a check that costs no query, the runtime integration — is ours. If none of that is worth paying for, use better-auth directly: it will not charge you.
What does it cost?
It comes with the Scale plan at CHF 99 a month, which also carries CHF 150 of included usage, the other modules on that tier, and the plan's support. No separate licence for the auth module, and no per-user pricing on top.
Which database does it need?
PostgreSQL — JAMflow Postgres, Neon, or an instance you operate yourself. Either way the schema is managed by Drizzle migrations, and it stays in your database rather than in ours.
See JAMflow Postgres →I already run better-auth. Can you take it over?
Yes. Either we take on the maintenance of the setup you have, or we work out a migration path onto the module — including the cases that never fit an off-the-shelf role system, which is most of why we built our own permission layer.
Does it work outside Nuxt?
A Nuxt module and an Elysia plugin, and those are the two today — same design, same permission layer, only the guard mounts differently.
And if I am on another stack?
Underneath it also assumes PostgreSQL. On Django, Laravel, or a Next.js app you are not going to move, there is nothing here to install: the most we can usefully do is look at the auth you have and say what we would change.
Do I have to be on the JAMflow platform?
No. The module runs on its own, on your infrastructure, against your database. Some things are tighter when the rest of the stack is ours — the Postgres cluster, the ingress we serve from — but none of that is a condition.
Tell us what your roles look like.
Describe how your product decides who may do what. If the module covers it, you get it. If it does not, you get a straight answer about what it would take — from the people who would have to do it.






