Production Deployment
Guidance for deploying Porta to production environments using Docker.
Docker Hub
The Porta Docker image is available on Docker Hub:
docker pull blendsdk/porta:latestNo git clone required — see the Quick Start for a standalone setup using just docker-compose.yml + .env.
Production Docker Compose
For production, remove MailHog and use a real SMTP relay. Here's a minimal production-ready compose file that can be used standalone (no repository clone needed):
services:
porta:
image: blendsdk/porta:latest
restart: unless-stopped
ports:
- "3000:3000"
environment:
NODE_ENV: production
PORT: "3000"
HOST: "0.0.0.0"
DATABASE_URL: postgresql://porta:${POSTGRES_PASSWORD}@postgres:5432/porta
REDIS_URL: redis://redis:6379
ISSUER_BASE_URL: https://auth.example.com
COOKIE_KEYS: ${COOKIE_KEYS}
SMTP_HOST: smtp.example.com
SMTP_PORT: "587"
SMTP_USER: ${SMTP_USER}
SMTP_PASS: ${SMTP_PASS}
SMTP_FROM: noreply@example.com
LOG_LEVEL: info
TWO_FACTOR_ENCRYPTION_KEY: ${TWO_FACTOR_ENCRYPTION_KEY}
TRUST_PROXY: "true"
PORTA_AUTO_MIGRATE: "false"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
start_period: 30s
retries: 3
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: porta
POSTGRES_USER: porta
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U porta"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
volumes:
pgdata:WARNING
Never use default passwords in production. Generate strong, unique values for POSTGRES_PASSWORD, COOKIE_KEYS, and TWO_FACTOR_ENCRYPTION_KEY.
Environment Variables
Required for Production
| Variable | Description | Example |
|---|---|---|
DATABASE_URL | PostgreSQL connection string | postgresql://porta:secret@postgres:5432/porta |
REDIS_URL | Redis connection string | redis://redis:6379 |
ISSUER_BASE_URL | Public-facing URL (must match your domain) | https://auth.example.com |
COOKIE_KEYS | Cookie signing key (≥32 random characters) | a1b2c3d4e5f6... |
TWO_FACTOR_ENCRYPTION_KEY | AES-256-GCM key (64 hex chars = 32 bytes) | 0123456789abcdef... |
SMTP_HOST | SMTP relay hostname | smtp.sendgrid.net |
SMTP_PORT | SMTP port | 587 |
SMTP_FROM | Sender email address | noreply@example.com |
Optional
| Variable | Default | Description |
|---|---|---|
NODE_ENV | production | Runtime mode |
PORT | 3000 | HTTP listen port |
HOST | 0.0.0.0 | HTTP listen address |
LOG_LEVEL | info | Log verbosity (debug, info, warn, error) |
TRUST_PROXY | false | Set to true when behind a TLS-terminating reverse proxy |
PORTA_AUTO_MIGRATE | false | Auto-run migrations on startup |
PORTA_WAIT_TIMEOUT | 60 | Seconds to wait for DB/Redis at startup |
Generating Secrets
# Cookie signing key (random 64-char string)
node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"
# Two-factor encryption key (64 hex chars)
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# Database password
node -e "console.log(require('crypto').randomBytes(24).toString('base64url'))"Secret Management
Production deployments must protect sensitive configuration values — database credentials, encryption keys, SMTP passwords, and cookie signing keys. Never commit secrets to version control or pass them as plain-text command-line arguments.
Environment File Security
The simplest approach: store secrets in an .env file with restrictive permissions.
# Create .env from the example template
cp .env.example .env
# Restrict read access to the file owner only
chmod 600 .env
# Edit with your production values
nano .envWARNING
Ensure .env is listed in .gitignore (it is by default in Porta). Never commit environment files containing real credentials.
Docker Secrets
For Docker Swarm deployments, use Docker Secrets to inject sensitive values as files rather than environment variables:
services:
porta:
image: blendsdk/porta:latest
environment:
# Non-secret configuration
NODE_ENV: production
ISSUER_BASE_URL: https://auth.example.com
# Read secrets from files mounted by Docker
DATABASE_URL_FILE: /run/secrets/database_url
COOKIE_KEYS_FILE: /run/secrets/cookie_keys
TWO_FACTOR_ENCRYPTION_KEY_FILE: /run/secrets/2fa_key
secrets:
- database_url
- cookie_keys
- 2fa_key
secrets:
database_url:
external: true
cookie_keys:
external: true
2fa_key:
external: trueCreate the secrets before deploying:
# Create secrets in Docker Swarm
echo "postgresql://porta:secret@postgres:5432/porta" | docker secret create database_url -
echo "your-cookie-signing-key-here" | docker secret create cookie_keys -
echo "0123456789abcdef..." | docker secret create 2fa_key -TIP
Porta's Docker entrypoint supports the _FILE suffix convention — if DATABASE_URL_FILE is set, Porta reads the secret from that file path instead of the DATABASE_URL environment variable.
Cloud Secret Managers
For cloud deployments, use your provider's secret management service:
| Provider | Service | Inject Via |
|---|---|---|
| AWS | Secrets Manager | ECS task definition secrets block, or Lambda env from SSM |
| GCP | Secret Manager | Cloud Run --set-secrets, or GKE volume mount |
| Azure | Key Vault | App Service Key Vault references, or AKS CSI driver |
Each service supports automatic rotation and audit logging. Refer to your provider's documentation for integration details.
HashiCorp Vault
For self-hosted or multi-cloud setups, HashiCorp Vault provides centralised secret management:
Agent sidecar pattern (recommended for containers):
- Run a Vault Agent sidecar alongside Porta
- The agent authenticates to Vault, fetches secrets, and writes them to a shared volume
- Porta reads secrets from the file paths via the
_FILEenv var convention
Environment injection pattern (simpler for VMs):
- Use
vault kv getor envconsul to inject secrets as environment variables before starting Porta - Example:
envconsul -prefix porta/config ./start.sh
Database
Migrations
Porta uses node-pg-migrate for schema management. Migration files are in the migrations/ directory inside the Docker image.
Running migrations manually:
# Via Docker exec
docker exec porta-app node dist/cli/index.js migrate up
# Check migration status
docker exec porta-app node dist/cli/index.js migrate statusAuto-migration: Set PORTA_AUTO_MIGRATE=true for the entrypoint to run migrations automatically on startup. This is convenient for initial setup but should be disabled in production once the schema is stable — run migrations explicitly during deployments.
Backup & Recovery
PostgreSQL is Porta's only durable data store — all organizations, users, clients, roles, signing keys, and audit logs live in PG. Regular, tested backups are essential.
Logical Backups (pg_dump)
Use pg_dump in custom format (-Fc) for the best balance of compression and flexibility:
# Full database backup (custom format, compressed)
docker exec porta-postgres pg_dump \
-U porta \
-Fc \
--no-owner \
--no-privileges \
porta > porta_$(date +%Y%m%d_%H%M%S).dump
# Plain SQL backup (human-readable, larger)
docker exec porta-postgres pg_dump \
-U porta \
--no-owner \
--no-privileges \
porta > porta_$(date +%Y%m%d_%H%M%S).sqlAutomate with cron — schedule daily backups and upload to secure storage:
# Example crontab entry — daily at 02:00 UTC
0 2 * * * docker exec porta-postgres pg_dump -U porta -Fc --no-owner porta > /backups/porta_$(date +\%Y\%m\%d).dumpPoint-in-Time Recovery (PITR)
For continuous backup with the ability to restore to any point in time, configure WAL archiving:
- Enable WAL archiving in
postgresql.conf:wal_level = replica archive_mode = on archive_command = 'cp %p /archive/%f' - Take periodic base backups with
pg_basebackup - Restore by replaying WAL files up to the desired timestamp
Managed Databases
Cloud-managed PostgreSQL services (AWS RDS, GCP Cloud SQL, Azure Database for PostgreSQL) provide automated PITR out of the box — typically with configurable retention up to 35 days. This is the simplest approach for production deployments.
Restore Procedures
# Restore from custom format dump
docker exec -i porta-postgres pg_restore \
-U porta \
--no-owner \
--no-privileges \
-d porta < porta_20260420_020000.dump
# Restore from plain SQL dump
docker exec -i porta-postgres psql -U porta porta < porta_20260420_020000.sqlTest Your Backups
A backup that has never been tested is not a backup. Periodically restore to a staging environment to verify data integrity and measure restore time.
Backup Encryption & Retention
- Encrypt at rest — Store backups in encrypted storage (S3 with SSE-KMS, GCS with CMEK, or gpg-encrypted files on disk)
- Encrypt in transit — Use TLS connections for any remote backup transfer
Suggested retention policy:
| Period | Frequency | Keep |
|---|---|---|
| Daily | Every day | 7 days |
| Weekly | Every Sunday | 4 weeks |
| Monthly | 1st of month | 12 months |
Adjust based on your compliance requirements and storage budget.
Access Controls
Porta stores signing keys as PEM-encoded private keys in the signing_keys database table. Until at-rest encryption (KEK) is implemented, restrict database-level access as an interim security measure.
Principle of Least Privilege
Create separate database roles for the application and for migrations:
-- Application role: can read/write data but NOT alter schema
CREATE ROLE porta_app LOGIN PASSWORD 'app-password-here';
GRANT CONNECT ON DATABASE porta TO porta_app;
GRANT USAGE ON SCHEMA public TO porta_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO porta_app;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO porta_app;
-- Migration role: can alter schema (used only during deployments)
CREATE ROLE porta_migrate LOGIN PASSWORD 'migrate-password-here';
GRANT CONNECT ON DATABASE porta TO porta_migrate;
GRANT ALL PRIVILEGES ON SCHEMA public TO porta_migrate;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO porta_migrate;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO porta_migrate;Update your connection strings:
- Application (
DATABASE_URL): useporta_app - Migrations (
porta migrate up): useporta_migrate
Restrict Signing Key Access
If you cannot use separate roles, at minimum restrict direct SELECT on the signing_keys table to prevent exposure through SQL injection or application bugs:
-- Revoke default access, then grant only to the app role
REVOKE ALL ON TABLE signing_keys FROM PUBLIC;
GRANT SELECT, INSERT, UPDATE ON TABLE signing_keys TO porta_app;TIP
This is an interim mitigation. A future release will add envelope encryption (KEK) so that signing keys are encrypted at rest in the database.
Redis
What Porta Stores in Redis
Porta uses Redis for short-lived, ephemeral data only:
| Data Type | Purpose | TTL |
|---|---|---|
| OIDC Sessions | Login interaction state | Minutes |
| Authorization Codes | PKCE auth code exchange | Minutes |
| OIDC Interactions | Consent/login flow state | Minutes |
| Rate Limit Counters | Brute-force protection | 60 seconds |
| Tenant Cache | Organization lookup cache | 5 minutes |
| Client Cache | Client metadata cache | 5 minutes |
| RBAC Cache | Role/permission lookup cache | 5 minutes |
Data Loss Tolerance
All Redis data is ephemeral and reconstructable. If Redis is flushed or restarted:
- Active login sessions are invalidated — users must re-authenticate
- Rate limit counters reset — temporarily allows more attempts (self-correcting)
- Cache entries are evicted — rebuilt on next access from PostgreSQL
No permanent data is lost. PostgreSQL is the sole source of truth for all durable state.
Recommended Settings
For production Redis, configure these settings in redis.conf or via container command:
# AOF persistence — provides durability across Redis restarts
# (belt-and-suspenders with the default RDB snapshots)
appendonly yes
appendfsync everysec
# Memory limit with LRU eviction — prevents Redis from consuming
# all available memory; safe because all data is cache/ephemeral
maxmemory 256mb
maxmemory-policy allkeys-lruDocker Compose example:
redis:
image: redis:7-alpine
command: >
redis-server
--appendonly yes
--maxmemory 256mb
--maxmemory-policy allkeys-lruNo Redis Backup Required
Since Redis contains only ephemeral data, backup is not required. PostgreSQL backup covers all durable state. Focus your backup strategy on PostgreSQL.
Security
HTTPS / Reverse Proxy
Porta listens on HTTP. Use a reverse proxy for TLS termination:
Nginx example:
server {
listen 443 ssl http2;
server_name auth.example.com;
ssl_certificate /etc/ssl/certs/auth.example.com.pem;
ssl_certificate_key /etc/ssl/private/auth.example.com-key.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Caddy example (automatic HTTPS):
auth.example.com {
reverse_proxy localhost:3000
}TRUST_PROXY Required
When running behind a TLS-terminating reverse proxy, you must set TRUST_PROXY=true. Without it, Porta cannot detect that the original connection was HTTPS — cookies will be set without the Secure flag, and OIDC login flows will fail because browsers silently drop insecure cookies on HTTPS pages.
TRUST_PROXY tells Koa to trust X-Forwarded-Proto and X-Forwarded-For headers from the proxy, so ctx.secure, ctx.protocol, and ctx.ip reflect the real client connection rather than the internal HTTP hop.
TIP
Make sure ISSUER_BASE_URL matches your public domain (e.g., https://auth.example.com). OIDC tokens include the issuer URL, and clients validate it.
Key Rotation
Regular key rotation limits the impact of key compromise. Porta supports zero-downtime rotation for all three secret types: signing keys, cookie keys, and client secrets.
Signing Key Rotation
Porta uses ES256 (ECDSA P-256) keys for JWT signing. Multiple keys can be active simultaneously — the newest key signs new tokens while older keys verify existing ones.
Zero-downtime rotation procedure:
# 1. List current signing keys
docker exec porta-app node dist/cli/index.js keys list
# 2. Generate a new signing key (becomes the active signing key)
docker exec porta-app node dist/cli/index.js keys generate
# 3. Verify the new key is active
docker exec porta-app node dist/cli/index.js keys listAfter generating a new key, the old key remains in the database for token verification. Wait for all existing tokens to expire before deactivating the old key:
| Token Type | Default TTL | Wait Before Deactivation |
|---|---|---|
| Access Token | 1 hour | 1 hour |
| Refresh Token | 14 days | 14 days |
| ID Token | 1 hour | 1 hour |
# 4. After the longest TTL has elapsed, deactivate the old key
docker exec porta-app node dist/cli/index.js keys rotateWARNING
Never deactivate the old key before its tokens expire — clients will receive invalid_token errors when presenting tokens signed with a deactivated key.
Recommended rotation schedule: Every 90 days, or immediately if a key is suspected to be compromised.
Cookie Key Rotation
COOKIE_KEYS is an ordered, comma-separated list. The first key signs new cookies; all keys are used for verification. This enables seamless rotation:
# 1. Generate a new cookie key
node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"
# Output: dG9wLXNlY3JldC1rZXktZXhhbXBsZQ...
# 2. Prepend the new key to COOKIE_KEYS (newest first)
# Before: COOKIE_KEYS=old-key-here
# After: COOKIE_KEYS=new-key-here,old-key-here
# 3. Restart Porta with the updated COOKIE_KEYS
docker compose -f docker/docker-compose.prod.yml restart porta
# 4. After session TTL expires (~24h), remove the old key
# Final: COOKIE_KEYS=new-key-hereTIP
When running multiple Porta replicas, update COOKIE_KEYS on all instances simultaneously — mismatched keys cause cookie verification failures.
Client Secret Rotation
OIDC clients can have multiple active secrets, enabling zero-downtime rotation for consuming services:
# 1. Generate a new secret for the client
docker exec porta-app node dist/cli/index.js client secret generate <client-id>
# Output: new secret value (save this — it cannot be retrieved later)
# 2. Update the consuming service/application with the new secret
# 3. Verify the consuming service works with the new secret
# 4. Revoke the old secret
docker exec porta-app node dist/cli/index.js client secret list <client-id>
docker exec porta-app node dist/cli/index.js client secret revoke <client-id> <old-secret-id>DANGER
Client secrets are displayed only once at generation time. Store the new secret securely in your consuming service before revoking the old one.
Health Checks & Readiness
Porta exposes two diagnostic endpoints:
GET /health — Liveness
Confirms the server process is running and can reach PostgreSQL and Redis.
Response (healthy — 200):
{
"status": "ok",
"checks": {
"database": "ok",
"redis": "ok"
}
}Response (unhealthy — 503):
{
"status": "error",
"checks": {
"database": "ok",
"redis": "error"
}
}GET /ready — Readiness
Verifies the server is ready to accept traffic by running a real DB query (SELECT 1) and a Redis PING, both with a 2-second timeout. Returns 200 when ready, 503 when not.
Use /ready for:
- Kubernetes readiness probes — prevents traffic routing before the server is fully ready
- Load balancer health checks — remove unhealthy instances from rotation
- Orchestrator startup checks — wait for full connectivity before considering the container healthy
Use /health for:
- Docker
HEALTHCHECK(already configured in the image) - Basic uptime monitoring (Uptime Robot, Pingdom, etc.)
Docker Compose example with readiness:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/ready"]
interval: 30s
timeout: 5s
start_period: 30s
retries: 3Kubernetes example:
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 10Logging
Porta uses pino for structured logging:
NODE_ENV | Format | Behavior |
|---|---|---|
development | Pretty-printed (pino-pretty) | Human-readable, colorized |
production | JSON (one line per entry) | Machine-parseable, suitable for log aggregators |
test | Silent | No log output |
PII Redaction
Porta automatically redacts sensitive fields from log output to prevent personally identifiable information (PII) from leaking into log aggregators. The following fields are replaced with [Redacted] in all log entries:
| Redacted Field | Reason |
|---|---|
password | User credentials |
token | Access/refresh tokens |
authorization | Bearer tokens in headers |
cookie | Session cookies |
refresh_token | OIDC refresh tokens |
client_secret | OIDC client secrets |
This redaction is always active regardless of NODE_ENV or LOG_LEVEL.
In production, pipe JSON logs to your log aggregator (ELK, Datadog, CloudWatch, etc.):
# View logs
docker compose -f docker/docker-compose.prod.yml logs -f porta
# With jq for readable JSON
docker logs porta-app | jq .Graceful Shutdown
Porta handles SIGTERM and SIGINT signals for graceful shutdown:
- The HTTP server stops accepting new connections
- In-flight requests are allowed to complete
- The server closes via a promisified
server.close() - Database and Redis connections are disconnected
- A 10-second kill switch forces exit if cleanup stalls
This ensures zero dropped requests during rolling deployments and container orchestration restarts.
TIP
Kubernetes sends SIGTERM before killing a pod. Set terminationGracePeriodSeconds: 15 (or higher) in your pod spec to give Porta enough time to drain.
Scaling
Porta is designed to be horizontally scalable:
- Stateless application — No in-memory sessions; all state is in PostgreSQL and Redis
- Shared database — All instances connect to the same PostgreSQL
- Shared cache — All instances share the same Redis for sessions and OIDC artifacts
- Health check — Each instance responds to
/healthindependently
To scale, run multiple Porta containers behind a load balancer:
services:
porta:
image: blendsdk/porta:latest
deploy:
replicas: 3
# ... (same config as above)WARNING
When running multiple replicas, ensure COOKIE_KEYS and TWO_FACTOR_ENCRYPTION_KEY are identical across all instances. These are encryption keys — different values will cause decryption failures.
Custom UI & Templates
Porta supports full customization of login pages and email templates. See the Custom UI Tutorial for a complete guide.
Per-Org Branding (Zero Code)
The fastest approach — set branding via the Admin API or CLI without touching any files:
porta org branding <org-id> \
--logo-url "https://cdn.example.com/logo.png" \
--primary-color "#E11D48" \
--company-name "Acme Corp"Custom Templates (Volume Mount)
For full control, mount a custom templates directory:
services:
porta:
image: blendsdk/porta:latest
volumes:
- ./my-templates:/app/templates/default:roWARNING
When mounting custom templates, include all template files (layouts, pages, partials, emails). Porta reads from the mounted directory exclusively — it does not merge with built-in defaults.
Custom Templates (Docker Image)
For immutable deployments, build a custom image:
FROM blendsdk/porta:latest
COPY my-templates/ /app/templates/default/Monitoring
Prometheus Metrics
When METRICS_ENABLED=true, Porta exposes a Prometheus-compatible GET /metrics endpoint using prom-client v15.
Available metrics:
| Metric | Type | Description |
|---|---|---|
porta_http_requests_total | Counter | Total HTTP requests (labels: method, status_code, path) |
| Default Node.js metrics | Various | CPU, memory, event loop lag, GC (via collectDefaultMetrics()) |
Enable in Docker Compose:
environment:
METRICS_ENABLED: "true"Prometheus scrape config:
scrape_configs:
- job_name: porta
scrape_interval: 15s
static_configs:
- targets: ['porta:3000']
metrics_path: /metricsWhen METRICS_ENABLED is false (default), the /metrics endpoint returns 404.
INFO
The metrics endpoint is unauthenticated. If exposing Porta directly to the internet, restrict access to /metrics via your reverse proxy or firewall.
General Monitoring
Beyond Prometheus metrics, monitor:
| Metric | Source | What to Watch |
|---|---|---|
| Health status | GET /health | Any non-200 response |
| Readiness | GET /ready | 503 responses indicate DB/Redis connectivity issues |
| Response times | Reverse proxy logs | P95 > 500ms |
| Error rate | Application logs (level: 50+) | Spike in errors |
| PostgreSQL connections | pg_stat_activity | Connection pool exhaustion |
| Redis memory | redis-cli info memory | Memory approaching limits |
| Disk usage | PostgreSQL data volume | Running out of space |
| Rate limit hits | Audit log security.rate_limited | Brute-force attempts |
| Account lockouts | Audit log user.locked | Credential-stuffing attacks |
Rate Limiting
Porta applies Redis-backed, per-IP rate limiting to sensitive endpoints:
| Scope | Limit | Window | Endpoints |
|---|---|---|---|
| Token endpoint | 30 requests | 5 minutes | POST /:orgSlug/auth/token |
| Admin API (write ops) | 60 requests | 60 seconds | POST/PUT/PATCH/DELETE /api/admin/* |
| Introspection | 100 requests | 60 seconds | POST /:orgSlug/auth/token/introspection |
| Login interactions | Per existing auth rate limiter | — | POST /:orgSlug/interaction/* |
When a rate limit is exceeded, the server returns 429 Too Many Requests with a Retry-After header. Rate limit events are logged to the audit trail as security.rate_limited.
INFO
Rate limit counters are stored in Redis and automatically expire. If Redis is restarted, counters reset — this temporarily allows more attempts but is self-correcting.
Account Lockout
Porta automatically locks user accounts after repeated failed login attempts to protect against brute-force and credential-stuffing attacks.
How It Works
- Each failed login increments a per-user
failed_login_countcounter in PostgreSQL - When the count reaches the threshold (default: 5 attempts), the account is auto-locked
- After the cooldown period (default: 15 minutes), the account auto-unlocks on the next login attempt
- A successful login resets the failed count to zero
Configuration
Account lockout thresholds are managed via system_config:
# View current settings
porta config get --key account_lockout_threshold
porta config get --key account_lockout_cooldown_minutes
# Change lockout threshold (default: 5)
porta config set --key account_lockout_threshold --value 10
# Change cooldown period in minutes (default: 15)
porta config set --key account_lockout_cooldown_minutes --value 30Security Design
- No information leakage — Locked accounts return the same error as invalid credentials, preventing account enumeration
- Audit logging — Every auto-lock event is logged as
user.lockedwith metadata indicating the trigger - Admin override — Administrators can manually unlock a user at any time via
porta user unlockor the Admin API
Request Size Limits
Porta enforces body parser size limits to prevent denial-of-service via oversized payloads:
| Content Type | Limit |
|---|---|
application/json | 100 KB |
application/x-www-form-urlencoded | 100 KB |
text/plain | 100 KB |
Requests exceeding these limits receive a 413 Payload Too Large response.
GDPR Compliance
Porta provides built-in support for GDPR data portability (Article 20) and right to erasure (Article 17).
Data Export (Article 20)
Export all personal data for a user in JSON format:
# Via CLI
porta user export --org-id <id> --user-id <id>
# Via API
GET /api/admin/organizations/:orgId/users/:userId/exportThe export includes: profile data, organization membership, role assignments, custom claim values, audit log entries, 2FA enrollment status, and active OIDC sessions.
Data Purge (Article 17)
Permanently anonymize and delete a user's personal data:
# Via CLI (requires confirmation)
porta user purge --org-id <id> --user-id <id>
# Via API
POST /api/admin/organizations/:orgId/users/:userId/purgeThe purge anonymizes the user record (replaces email, name, etc. with anonymized values) and deletes all associated data (roles, claims, tokens, 2FA enrollment, audit metadata) in a single database transaction.
DANGER
Data purge is irreversible. The CLI prompts for confirmation; use --force to skip. Super-admin users cannot be purged as a safety measure.
Audit Retention
Configure automatic cleanup of old audit log entries:
# Set retention period (in days)
porta config set --key audit_retention_days --value 365
# Run cleanup (deletes entries older than retention period)
porta audit cleanupSee Audit Log API and CLI Infrastructure for details.