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

Integrations Reference

Last Updated: 2026-04-25

Overview

Porta integrates with several external systems and libraries. This document describes each integration's configuration, usage patterns, and operational characteristics.

Core integrations: PostgreSQL, Redis, SMTP, node-oidc-provider

Admin GUI integrations: FluentUI v9, React Query, Vite, Playwright

PostgreSQL 16

Purpose

Primary persistent data store for all Porta data:

  • Organization, application, client, user records
  • RBAC roles, permissions, and assignments
  • Custom claim definitions and values
  • Two-factor authentication settings
  • Auth tokens (magic links, password resets, invitations)
  • Audit log entries
  • System configuration
  • Signing keys (encrypted at rest)
  • Long-lived OIDC artifacts (AccessToken, RefreshToken, Grant)

Connection

ParameterSourceDescription
Connection stringDATABASE_URL env varFull PostgreSQL connection URL
Driverpg (node-postgres)Native PostgreSQL client
Pool sizeDefault (pg default: 10)Connection pool managed by pg.Pool

Connection module: src/lib/database.ts

typescript
import { getPool } from '../lib/database.js';

const pool = getPool();
const result = await pool.query('SELECT $1::text AS message', ['hello']);

Query Patterns

All queries use parameterized SQL — no raw string interpolation:

typescript
// Standard CRUD
await pool.query(
  'INSERT INTO organizations (id, name, slug, status) VALUES ($1, $2, $3, $4)',
  [id, name, slug, 'active']
);

// Dynamic UPDATE (service layer builds SET clause safely)
const setClauses = ['name = $2', 'updated_at = NOW()'];
await pool.query(
  `UPDATE organizations SET ${setClauses.join(', ')} WHERE id = $1`,
  [id, name]
);

// Cursor-based pagination
await pool.query(
  `SELECT * FROM organizations 
   WHERE (name, id) > ($1, $2) 
   ORDER BY name ASC, id ASC 
   LIMIT $3`,
  [lastValue, lastId, limit]
);

Extensions

ExtensionMigrationPurpose
pgcrypto001gen_random_uuid() for UUID primary keys
citext001Case-insensitive text for emails and slugs

Connection Lifecycle

  1. Startup: Pool created in src/index.ts, validated with SELECT 1
  2. Runtime: Queries use pool.query() (auto-acquire/release connections)
  3. Health check: GET /health runs SELECT 1 to verify connectivity
  4. Shutdown: Pool closed gracefully on SIGTERM/SIGINT

Migrations

  • Tool: node-pg-migrate (programmatic runner in src/lib/migrator.ts)
  • Files: 19 SQL migrations in migrations/ directory
  • Auto-run: Entrypoint script runs migrations on container start
  • CLI: porta migrate up/down/status for manual control

Redis 7

Purpose

In-memory data store for performance-sensitive and ephemeral data:

  • OIDC sessions — Short-lived artifacts (Session, Interaction, AuthorizationCode, etc.)
  • Tenant cache — Organization lookup by slug/ID
  • Entity cache — User, client, role, permission lookups
  • Rate limiting — Sliding window counters for auth endpoints
  • Claim cache — Custom claim definitions and user values

Connection

ParameterSourceDescription
Connection stringREDIS_URL env varFull Redis connection URL
DriverioredisFull-featured Redis client
ReconnectionAuto-reconnectBuilt-in exponential backoff

Connection module: src/lib/redis.ts

typescript
import { getRedis } from '../lib/redis.js';

const redis = getRedis();
await redis.set('key', 'value', 'EX', 300); // 5-minute TTL
const value = await redis.get('key');

Data Patterns

OIDC Adapter (Short-Lived Artifacts)

oidc:{model}:{uid}           → JSON payload with TTL
oidc:{model}:{uid}:grant     → Grant ID lookup
oidc:grant:{grantId}         → Set of artifact UIDs (for cascade deletion)
oidc:user_code:{userCode}    → UID lookup (device flow)

Models stored in Redis: Session, Interaction, AuthorizationCode, ReplayDetection, ClientCredentials, PushedAuthorizationRequest.

Tenant Cache

org:slug:{slug}    → JSON Organization object (TTL: configurable)
org:id:{orgId}     → JSON Organization object (TTL: configurable)

Cache-first strategy: Redis check → PostgreSQL fallback → cache on miss.

Rate Limiting

rate:{endpoint}:{identifier}  → Counter (INCR + EXPIRE)

Sliding window implementation using Redis INCR and EXPIRE.

Cache Invalidation

  • On write: Service layer invalidates cache after successful DB writes
  • Graceful degradation: Cache miss falls through to PostgreSQL
  • Never block: Cache operations don't fail requests

Data Persistence

Redis data is ephemeral — no persistence configuration needed:

  • Cache data rebuilds from PostgreSQL on miss
  • OIDC sessions have natural TTLs
  • Rate limit counters expire automatically
  • Redis restart causes temporary cache misses (not data loss)

Connection Lifecycle

  1. Startup: Client created in src/index.ts, validated with ping
  2. Runtime: All Redis operations use the shared client
  3. Health check: GET /health runs ping to verify connectivity
  4. Shutdown: Client disconnected gracefully on SIGTERM/SIGINT

SMTP (Email)

Purpose

Email delivery for authentication workflows:

  • Magic link emails (passwordless login)
  • Password reset emails
  • User invitation emails
  • Email OTP for two-factor authentication

Connection

ParameterSourceDescription
HostSMTP_HOST env varSMTP server hostname
PortSMTP_PORT env var (default: 587)SMTP server port
UsernameSMTP_USER env var (optional)SMTP auth username
PasswordSMTP_PASS env var (optional)SMTP auth password
FromSMTP_FROM env varSender email address
DriverNodemailerStandard Node.js SMTP transport

Module: src/auth/email-transport.ts (transport abstraction), src/auth/email-service.ts (high-level API)

Email Flow

  1. Service triggers email (e.g., magic link requested)
  2. Renderer (src/auth/email-renderer.ts) renders Handlebars template with i18n
  3. Transport (src/auth/email-transport.ts) sends via Nodemailer
  4. SMTP server delivers the email

Templates

Email templates live in templates/default/ with locale-specific translations in locales/default/{locale}/.

Development

In development, MailHog captures all emails:

  • SMTP: localhost:1025
  • Web UI: http://localhost:8025 (view captured emails in browser)

node-oidc-provider 9.x

Purpose

OpenID Connect protocol engine — handles all OIDC-compliant authentication flows:

  • Authorization Code (with PKCE)
  • Client Credentials
  • Refresh Token
  • Discovery (/.well-known/openid-configuration)
  • JWKS (/.well-known/jwks)
  • Token introspection and revocation

Integration Architecture

Configuration

The OIDC provider is configured in src/oidc/configuration.ts:

FeatureConfiguration
Signing algorithmES256 (ECDSA P-256) only
PKCEEnforced for public clients (S256 method)
Scopesopenid, profile, email, offline_access + custom
ClaimsStandard OIDC claims + RBAC roles + custom claims
Grant typesauthorization_code, refresh_token, client_credentials
Token formatJWT (signed with ES256)
InteractionsCustom login/consent pages
TTLsLoaded from system_config table at startup

Adapter Strategy

The adapter factory (src/oidc/adapter-factory.ts) routes OIDC models to the appropriate storage backend:

ModelAdapterStorage
SessionRedissrc/oidc/redis-adapter.ts
InteractionRedissrc/oidc/redis-adapter.ts
AuthorizationCodeRedissrc/oidc/redis-adapter.ts
ReplayDetectionRedissrc/oidc/redis-adapter.ts
ClientCredentialsRedissrc/oidc/redis-adapter.ts
PushedAuthorizationRequestRedissrc/oidc/redis-adapter.ts
AccessTokenPostgreSQLsrc/oidc/postgres-adapter.ts
RefreshTokenPostgreSQLsrc/oidc/postgres-adapter.ts
GrantPostgreSQLsrc/oidc/postgres-adapter.ts

Client Discovery

src/oidc/client-finder.ts — Called by node-oidc-provider when it needs client metadata:

  1. Query clients table by client_id
  2. Load active secrets for the client
  3. Map to node-oidc-provider's expected client metadata format
  4. Include client_secret (SHA-256 pre-hash) for secret comparison

Account Claims

src/oidc/account-finder.ts — Called by node-oidc-provider when building ID tokens:

  1. Load user by ID from the users service
  2. Build standard OIDC claims (profile, email)
  3. Load RBAC roles for the user
  4. Load custom claim values for the user
  5. Return scope-filtered claims

Interaction Handling

Custom login and consent pages are implemented as Koa routes (src/routes/interactions.ts) that integrate with node-oidc-provider's interaction system:

  1. Provider redirects to /interaction/:uid for login/consent
  2. Custom Handlebars templates render the UI
  3. User submits credentials → validated against user service
  4. 2FA checked if required by org policy
  5. Provider resumes the OIDC flow

Mounting

The provider is mounted as Koa middleware under /:orgSlug/* prefix in src/server.ts, after the tenant resolver middleware sets the organization context.

FluentUI v9

Purpose

Microsoft's enterprise-grade React component library, used for all Admin GUI UI components.

Key Dependencies

PackagePurpose
@fluentui/react-componentsCore component library (Button, Input, Dialog, etc.)
@fluentui/react-iconsFluent icon set
@fluentui/react-datepicker-compatDate picker component

Usage Pattern

The Admin GUI wraps the entire SPA in a FluentProvider with theme support (light/dark):

tsx
import { FluentProvider, webLightTheme } from '@fluentui/react-components';

<FluentProvider theme={webLightTheme}>
  <App />
</FluentProvider>

All UI components use FluentUI primitives — no custom CSS frameworks (Tailwind, Bootstrap, etc.).

React Query (TanStack Query v5)

Purpose

Server state management for the Admin GUI SPA. Handles data fetching, caching, cache invalidation, optimistic updates, and background refetching.

Usage Pattern

Each entity domain has its own React Query hook module in admin-gui/src/client/api/:

typescript
// Example: useOrganizations hook module
export function useOrganizations(params?: ListParams) {
  return useQuery({
    queryKey: ['organizations', params],
    queryFn: () => api.get('/api/admin/organizations', { params }),
  });
}

export function useCreateOrganization() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (data: CreateOrgInput) => api.post('/api/admin/organizations', data),
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['organizations'] }),
  });
}

13 hook modules cover all entity domains: organizations, applications, clients, users, roles, permissions, claims, config, keys, audit, sessions, stats, and export.

Vite

Purpose

Build tool and dev server for the Admin GUI React SPA.

Configuration

Vite config (admin-gui/vite.config.ts):

  • Dev proxy: /api and /auth requests proxied to BFF (port 4002)
  • Build output: admin-gui/dist/client/ — static assets served by the BFF in production
  • React plugin: @vitejs/plugin-react for JSX/TSX compilation

Playwright

Purpose

E2E browser testing for the Admin GUI. 204 tests across 23 spec files.

Test Architecture

  • In-process servers: Porta and BFF start in the test process on ephemeral ports
  • Magic-link auth: Tests extract magic-link tokens from MailHog API
  • Session persistence: storageState stores authenticated session across tests
  • Seed data: Test fixtures create organizations, applications, users, etc.

Integration Summary

Released under the MIT License.