⚠️ Porta is in beta — APIs and features may change before v1.0
Skip to content

System Overview

Last Updated: 2026-05-07

High-Level Architecture

Porta is a multi-tenant OIDC identity provider built on Koa + node-oidc-provider + TypeScript. It serves as a complete authentication and user management platform with organization-scoped tenancy, RBAC, custom claims, and two-factor authentication.

Component Overview

Core Runtime

ComponentTechnologyPurpose
HTTP ServerKoa 3.xRequest handling, middleware pipeline
OIDC Enginenode-oidc-provider 9.xOpenID Connect protocol implementation
DatabasePostgreSQL 16 (pg)Persistent storage, long-lived OIDC artifacts
Cache/SessionsRedis 7 (ioredis)Short-lived OIDC artifacts, tenant cache, rate limits
LoggerpinoStructured logging (JSON in prod, pretty in dev)
ConfigzodEnvironment validation with fail-fast semantics
Signingjose + cryptoES256 (ECDSA P-256) token signing
CLIyargsAdmin command-line interface
Admin GUI SPAReact 19 + FluentUI v9Browser-based admin dashboard (standalone package)
Admin GUI BFFKoa (standalone process)Backend-for-frontend: OIDC auth (PKCE public client), in-memory sessions, API proxy
Admin GUI BuildViteSPA bundler and dev server

Domain Modules

Porta follows a modular domain architecture where each business domain is encapsulated in its own directory under src/:

ModuleDirectoryResponsibility
Organizationssrc/organizations/Tenant management, status lifecycle, branding
Applicationssrc/applications/SaaS product definitions, module grouping
Clientssrc/clients/OIDC client registration, secret management
Userssrc/users/User accounts, passwords, status lifecycle
Authsrc/auth/Authentication workflows, magic links, email, templates
RBACsrc/rbac/Roles, permissions, user-role assignments
Custom Claimssrc/custom-claims/Claim definitions, user claim values
Two-Factorsrc/two-factor/TOTP, email OTP, recovery codes
CLIsrc/cli/Admin CLI with dual-mode bootstrap

Each domain module follows a consistent internal structure:

src/<module>/
├── index.ts          # Barrel export (public API)
├── types.ts          # Domain types, interfaces, row mapping
├── errors.ts         # Domain-specific error classes
├── repository.ts     # PostgreSQL CRUD operations
├── cache.ts          # Redis caching layer
├── service.ts        # Business logic, validation, orchestration
├── slugs.ts          # Slug generation and validation (where applicable)
└── validators.ts     # Input validation (where applicable)

Admin GUI Module

The Admin GUI is a standalone package at packages/porta-admin-gui/ (@portaidentity/admin-gui). It is a Koa BFF + React/Vite SPA that authenticates as an OIDC public client using Authorization Code + PKCE. It uses in-memory sessions (no Redis dependency) with SameSite=Lax cookies.

LayerTechnologyPurpose
React SPA (packages/porta-admin-gui/src/client/)React 19, FluentUI v9, React Router, React QueryBrowser-based admin dashboard
Koa BFF (packages/porta-admin-gui/src/)Koa, in-memory sessionsOIDC auth (PKCE), API proxy with Bearer token injection

The SPA communicates exclusively through the BFF — it never talks to the Porta Admin API directly. The BFF handles Bearer token injection and OIDC token refresh.

Launching the Admin GUI:

  • Development: yarn dev from root starts both the Porta server and Admin GUI (BFF + Vite SPA) concurrently
  • Standalone: porta gui (via @portaidentity/cli) or npx @portaidentity/admin-gui
  • Server URL resolution: CLI flag --server > PORTA_SERVER env var > ~/.porta/credentials.json

SPA Architecture:

packages/porta-admin-gui/src/client/
├── api/             # Typed API client + React Query domain hook modules
├── components/      # Reusable UI components (StatusBadge, ConfirmDialog, etc.)
├── hooks/           # Hooks (useAuth, useTheme, useToast, useCopyToClipboard)
├── layouts/         # AppShell layout
├── pages/           # Route page components
└── types.ts         # Shared client-side type definitions

Application Startup Sequence

The application starts via src/index.ts in a strict 7-step sequence:

Fail-fast principle: If any startup step fails (DB connection, Redis connection, config validation), the process logs a fatal error and calls process.exit(1).

Middleware Stack

The Koa middleware stack is assembled in src/server.ts in this precise order:

Key Middleware Details

MiddlewareFilePurpose
Error Handlererror-handler.tsGlobal try/catch, hides internal details for 5xx
Request Loggerrequest-logger.tsUUID request ID, logs method/url/status/duration
Security Headerssecurity-headers.tsCSP default-src 'none', X-Frame-Options, HSTS, etc.
Metricsmetrics.tsPrometheus metrics at GET /metrics (optional)
Root Pageroot-page.tsNeutral /, /robots.txt, /favicon.ico (no product leakage)
Health Checkhealth.tsDB + Redis connectivity check at /health
Readinessready.tsReadiness probe for container orchestration
Admin Authadmin-auth.tsJWT Bearer validation for /api/admin/* routes
Admin CORSadmin-cors.tsCORS handling for /api/admin/* (configurable origins)
Admin Rate Limiteradmin-rate-limiter.tsRate limiting for admin API endpoints
Require Permissionrequire-permission.tsGranular RBAC permission checks for admin routes
Token Rate Limitertoken-rate-limiter.tsRate limiting for token endpoints
Tenant Resolvertenant-resolver.tsCache-first org lookup from URL slug
Client Secret Hashclient-secret-hash.tsSHA-256 pre-hash for client_secret_post
OIDC CORSoidc-cors.tsCORS handling for OIDC endpoints

Multi-Tenancy Model

Porta uses path-based multi-tenancy where each organization gets its own OIDC issuer URL:

https://auth.example.com/{orgSlug}/.well-known/openid-configuration
https://auth.example.com/{orgSlug}/auth
https://auth.example.com/{orgSlug}/token

The tenant resolver middleware (src/middleware/tenant-resolver.ts) uses a cache-first strategy:

  1. Check Redis cache for org by slug
  2. On miss → query PostgreSQL
  3. Cache the result in Redis
  4. Set ctx.state.organization for downstream handlers
  5. Return appropriate HTTP status based on org status:
    • active → proceed
    • suspended → 403
    • archived → 410
    • Not found → pass through (no match)

Graceful Shutdown

On SIGTERM or SIGINT:

  1. Stop accepting new connections
  2. Close the HTTP server
  3. Disconnect Redis
  4. Close the PostgreSQL pool
  5. Force exit after 10 seconds if still hanging

Released under the MIT License.