A self-hosted, multi-tenant identity provider (IdP) with a built-in OpenID
Connect / OAuth 2.0 authorization server. It owns login, MFA, RBAC, sessions,
and token issuance for a whole platform of applications: it signs its own RS256
JWTs and publishes the corresponding public keys at /.well-known/jwks.json, so
consumer services verify those tokens offline — with jose or
passport-jwt + a JWKS provider — without sharing a database with it.
It is not a gateway or proxy sitting in front of some other auth system, and your traffic does not pass through it. Applications call it to authenticate a human or a machine client, get a token back, and verify that token themselves from then on.
A few internal identifiers still carry the project's earlier working name,
auth-gateway — the admin panel's package (auth-gateway-web), the
PostgreSQL database name, Docker service names, and the JWKS kid. That's
intentional (some of those are baked into already-issued tokens), not
leftover inconsistency.
Authentication
- Email + password, hashed with argon2id (
@node-rs/argon2), with transparent rehash of legacy bcrypt hashes on successful login (ADR-0002), and password strength scoring viazxcvbn - Passwordless login: magic link (
/api/auth/magic-link) and email OTP (/api/auth/email-otp/*) - Social SSO with Google and GitHub (Passport) — tenant-bound opaque
statein Redis, provider tokens encrypted at rest with AES-256-GCM (ADR-0013/0014) - Email verification, password reset, single-use invitations, and tri-state
registration policies per tenant (
OPEN/INVITE_ONLY/CLOSED, ADR-0011) - Account lockout after 10 failed attempts (15 min), on top of a Redis-backed per-identity login limiter
MFA
- TOTP (
otplib) with secrets encrypted at rest (AES-256-GCM, ADR-0008) and replay protection in Redis - 8 single-use recovery codes, hashed, regenerable
- Passkeys / WebAuthn (
@simplewebauthn/server) usable at the login challenge mfaRequiredenforced per tenant
Tokens and sessions
- RS256 access tokens; JWKS exposed at
/.well-known/jwks.jsonwithkid-based key rotation — current + previous key served simultaneously, driven by an operator runbook (make rotate-key, ADR-0025) - Refresh-token rotation with reuse detection: a token replayed outside the
grace window revokes the whole token family and writes a
TOKEN_REUSE_DETECTEDaudit event (ADR-0009/0010) - Immediate access-token revocation via a Redis denylist keyed by the stable
sidclaim (ADR-0003), so logout is selective and effective before expiry - Per-user session listing and revocation, plus scheduled cleanup of expired sessions
Multi-tenancy and authorization
- A user can belong to many tenants with a different role in each
- Roles + CASL permissions, system roles plus custom roles per tenant
- PostgreSQL row-level security, enforced at the database through a dual-client setup: a BYPASSRLS client for the genuinely cross-tenant auth flows and an RLS-bound client for everything tenant-scoped (ADR-0007)
- Cross-tenant super-admin endpoints under
/api/admin/*
OIDC / OAuth 2.0
- A full OIDC provider (
node-oidc-provider) mounted at/oidc, discovery at/oidc/.well-known/openid-configuration, gated at runtime byOIDC_ENABLED(ADR-0012/0015) authorization_codewith PKCE requiredclient_credentialsfor machine-to-machine clients — the native OIDC grant plus a legacyPOST /api/oauth/tokenendpoint kept alive during the migration window (ADR-0006/0023)- Per-tenant OIDC issuer resolved by domain (ADR-0019), with optional tenant vanity domains provisioned as Cloudflare custom hostnames
Operations
- Event-driven audit log with a durable outbox table and a drain worker (ADR-0024)
- Transactional email with per-tenant branding and per-tenant/locale copy overrides
/health(Terminus) with PostgreSQL and Redis checks- Rate limiting via
@nestjs/throttlerglobally and per endpoint, plus Redis-backed limiters on login and the OIDC token endpoint
| Layer | Choice |
|---|---|
| API | NestJS 11 (Express 5), TypeScript strict |
| Database | PostgreSQL 16 + Prisma 7 (@prisma/adapter-pg), RLS policies |
| Cache / ephemeral state | Redis 7 (ioredis) |
| OIDC | node-oidc-provider 9 |
| Frontends | React 19 + Vite, Tailwind CSS 4 |
| Tooling | pnpm 10, Biome, Vitest 4, Node >= 24 |
.
├── src/ # NestJS API — the IdP itself, serves :3200
├── prisma/ # Prisma 7 schema, migrations, rls.sql, seed
├── apps/
│ ├── landing/ # @queres/landing — public site + end-user auth UI (login,
│ │ # register, MFA, passkeys, self-service tenant dashboard)
│ └── web/ # auth-gateway-web — admin panel (tenants, users, roles,
│ # sessions, audit logs)
├── docs/ # architecture, API, integration, database, ADRs
├── docker/ # database init scripts
└── deploy/ # deployment scaffolding
Both frontends are independent SPAs — see apps/landing/README.md
and apps/web/README.md. Neither is served by the Node
container: the Dockerfile runtime only packages the NestJS dist/, and in a
typical deployment a reverse proxy does path-based routing on one domain
(/ → landing, /app/ → admin panel, /api/ → the API). docker compose up
brings up PostgreSQL and Redis, not the frontends.
Requires Node >= 24, pnpm >= 10, and Docker. Start from the example
environment file (cp .env.example .env) and fill in at least the database
URLs, TOTP_ENCRYPTION_KEY, and the Redis URL.
pnpm install && docker compose up -d
make generate-keys
DATABASE_URL="$DATABASE_URL_ADMIN" npx prisma migrate dev # app_user has no CREATEDB → P3014
npx prisma db execute --file prisma/rls.sql && npx prisma db seed
pnpm run start:dev # :3200The two frontends run separately, each with its own Vite dev server proxying to
the API on :3200:
pnpm --filter @queres/landing dev # landing + end-user auth UI
pnpm --filter auth-gateway-web dev # admin panelSeed credentials: admin@auth-gateway.local / changeme.
In local development, email OTP / magic-link / invitation tokens never leave
your machine — read them from the verification_tokens table or point
SMTP_HOST at a local mailpit.
DATABASE_URL=postgres://x TOTP_ENCRYPTION_KEY=<64hex> pnpm testUnit tests live next to the code. The test:integration, test:e2e, and
test:security suites are separate Vitest configs and need a real
PostgreSQL/Redis (docker compose up -d).
| Document | What's in it |
|---|---|
docs/ARCHITECTURE.md |
Module and guard chain, auth/refresh/reuse-detection flows, multi-tenant data model, RLS |
docs/API.md |
REST endpoint reference with request/response shapes |
docs/INTEGRATION.md + CONTRACT.md |
How a downstream app consumes this IdP: JWKS verification, JWT claims, the public contract |
docs/DATABASE.md |
Schema and row-level security details |
docs/adr/ |
Architecture decision records — the why behind the non-obvious choices |
CLAUDE.md |
Living map of invariants and "don't do this" gotchas |
ROADMAP.md |
Current status and backlog |
See CONTRIBUTING.md for dev setup, coding standards, test
expectations, and the branch/PR flow. Auth, sessions, and tenant isolation are
the core of what this service does, so changes there are held to a higher bar.
Found a security issue? Do not open a public issue or PR — follow
SECURITY.md.
Copyright (C) 2026 soy-chrislo. Licensed under AGPL-3.0. Because of the AGPL's network-use clause (section 13), if you modify QUERES and run the modified version as a network service, you must offer its users the corresponding source of your modified version.