Developer Guide¶
This guide walks you through building, running, and deploying apps on Plattr. By the end, you'll have a full-stack app with a database, object storage, authentication, and an auto-generated REST API — all from a single plattr.yaml file.
Prerequisites¶
- Node.js 18+ and npm
- Docker (Docker Desktop or colima)
- Kind —
go install sigs.k8s.io/kind@latestorbrew install kind - kubectl —
brew install kubectlor install guide - Dagger CLI —
curl -fsSL https://dl.dagger.io/dagger/install.sh | sh - Plattr CLI —
npm install -g @plattr/cli
Creating a New App¶
The interactive wizard asks:
1. App name — lowercase, alphanumeric, hyphens allowed (e.g., my-app)
2. Framework — auto-detected, or choose: nextjs, rails, static, docker
3. Database — enable PostgreSQL?
4. Storage — enable S3-compatible object storage?
5. Auth — enable Keycloak authentication?
6. Redis — enable Redis cache?
7. OpenSearch — enable OpenSearch?
This creates two files:
- plattr.yaml — your app configuration
- .github/workflows/plattr-deploy.yml — CI/CD pipeline
Example plattr.yaml¶
name: my-app
framework: nextjs
database:
enabled: true
storage:
enabled: true
buckets:
- name: uploads
public: false
- name: assets
public: true
auth:
enabled: true
providers:
- google
- github
redis:
enabled: true
search:
enabled: true
local:
port: 3000
See the full plattr.yaml Reference for all options.
Local Development¶
This sets up your entire infrastructure stack locally on a Kind (Kubernetes in Docker) cluster:
- Creates a Kind cluster and local container registry (first run only)
- Deploys infrastructure services as Kubernetes pods
- Starts port-forwards so services are accessible on localhost
- Writes environment variables to
.plattr/{appName}.env
After plattr dev completes, source the env file and start your dev server:
Local Services¶
| Service | Port | Condition |
|---|---|---|
| PostgreSQL | 5432 | Always |
| PostgREST | 3001 | database.enabled: true |
| MinIO | 9000, 9001 | storage.enabled: true |
| Keycloak | 8080 | auth.enabled: true |
| Redis | 6379 | redis.enabled: true |
| OpenSearch | 9200 | search.enabled: true |
| OpenSearch Dashboards | 5601 | search.enabled: true |
All services start only if enabled in plattr.yaml. A static site with no database won't start PostgreSQL.
Managing Infrastructure¶
# Check infrastructure status
plattr infra status
# Stop infrastructure (data preserved, saves resources)
plattr infra stop
# Restart stopped infrastructure
plattr infra start
# Delete everything (cluster, registry, all data)
plattr infra destroy
Custom Port¶
State Files¶
plattr dev creates a .plattr/ directory in your project with:
- {appName}.env — environment variables you can source
- {appName}.pids — port-forward process IDs (managed automatically)
Add .plattr/ to your .gitignore.
Environment Variables¶
Plattr automatically provides these env vars in .plattr/{appName}.env:
Database (when database.enabled: true)¶
| Variable | Local Value | Description |
|---|---|---|
DATABASE_URL |
postgresql://plattr:localdev@127.0.0.1:5432/plattr?search_path=my_app |
Full connection string |
POSTGREST_URL |
http://127.0.0.1:3001 |
PostgREST endpoint |
Storage (when storage.enabled: true)¶
| Variable | Local Value | Description |
|---|---|---|
S3_ENDPOINT |
http://127.0.0.1:9000 |
S3 endpoint |
S3_ACCESS_KEY |
minioadmin |
Access key |
S3_SECRET_KEY |
minioadmin |
Secret key |
S3_REGION |
us-east-1 |
Region |
S3_BUCKET_UPLOADS |
my-app-uploads |
Bucket name (one per bucket in config) |
Bucket env var names are derived from the bucket name: uploads becomes S3_BUCKET_UPLOADS, user-files becomes S3_BUCKET_USER_FILES.
Auth (when auth.enabled: true)¶
| Variable | Local Value | Description |
|---|---|---|
AUTH_ISSUER_URL |
http://127.0.0.1:8080/realms/my-app |
OIDC issuer URL |
AUTH_CLIENT_ID |
my-app-app |
OIDC client ID |
Redis (when redis.enabled: true)¶
| Variable | Local Value | Description |
|---|---|---|
REDIS_URL |
redis://127.0.0.1:6379 |
Redis connection URL |
OpenSearch (when search.enabled: true)¶
| Variable | Local Value | Description |
|---|---|---|
OPENSEARCH_URL |
http://127.0.0.1:9200 |
OpenSearch endpoint |
OPENSEARCH_DASHBOARDS_URL |
http://127.0.0.1:5601 |
OpenSearch Dashboards UI |
Production Equivalents¶
| Variable | Local Value | Production Value |
|---|---|---|
DATABASE_URL |
postgresql://plattr:localdev@127.0.0.1:5432/... |
postgresql://{app}_app:{random}@aurora:5432/... |
POSTGREST_URL |
http://127.0.0.1:3001 |
https://{domain}/api/rest |
POSTGREST_INTERNAL_URL |
(not set) | http://localhost:3001 |
S3_ENDPOINT |
http://127.0.0.1:9000 |
(AWS S3 default) |
AUTH_ISSUER_URL |
http://127.0.0.1:8080/realms/{app} |
https://auth.{baseDomain}/realms/{app} |
REDIS_URL |
redis://127.0.0.1:6379 |
redis://{managed-redis}:6379 |
OPENSEARCH_URL |
http://127.0.0.1:9200 |
https://{managed-opensearch}:443 |
In production, POSTGREST_INTERNAL_URL connects directly to the PostgREST sidecar (same pod, no network hop). Use it for server-side calls. Use POSTGREST_URL for client-side/browser calls.
Testing¶
Local Tests¶
Auto-detects your test runner and runs tests using the environment from .plattr/{appName}.env. Requires infrastructure to be running (plattr dev first).
Supported test runners (auto-detected):
- Vitest — vitest.config.{ts,js,mts}
- Jest — jest.config.{ts,js,mjs}
- npm test — package.json test script
- RSpec — spec/**/*.rb
- Minitest — test/**/*.rb
Ephemeral CI Tests¶
Runs Dagger-based tests with clean infrastructure (fresh database, fresh state). Use this in CI pipelines or when you want guaranteed clean state.
Database Workflows¶
Running Migrations¶
# Auto-detect migration engine
plattr db migrate
# Specify engine explicitly
plattr db migrate --engine prisma
plattr db migrate --engine knex
plattr db migrate --engine raw
Supported engines:
- prisma — runs npx prisma migrate deploy
- knex — runs npx knex migrate:latest
- raw — executes .sql files from the migrations directory
Seeding Data¶
Interactive Shell (Local)¶
Opens a psql session connected to your local database.
Connect to Remote Database¶
Retrieves the DATABASE_URL from the Kubernetes Secret and opens a psql session.
Reset Local Database¶
Deletes the persistent volume and restarts PostgreSQL. Run plattr dev to recreate schemas.
Schema and Roles¶
Plattr creates these PostgreSQL objects for your app:
- Schema:
{env}_{app_name}(e.g.,prod_my_app,staging_my_app) - App role:
{env}_{app_name}_app— full CRUD access, used by your app - Anon role:
{env}_{app_name}_anon— no permissions by default, used by PostgREST
To expose a table through PostgREST, grant permissions to the anon role in your migrations:
PostgREST (Auto-Generated REST API)¶
When database.enabled: true, you get an auto-generated REST API from your database schema. PostgREST introspects your tables and exposes them as RESTful endpoints.
Local Usage¶
# List all rows in the "todos" table
curl http://localhost:3001/todos
# Filter
curl "http://localhost:3001/todos?completed=eq.false"
# Insert
curl -X POST http://localhost:3001/todos \
-H "Content-Type: application/json" \
-d '{"title": "Buy milk", "completed": false}'
# Update
curl -X PATCH "http://localhost:3001/todos?id=eq.1" \
-H "Content-Type: application/json" \
-d '{"completed": true}'
# Delete
curl -X DELETE "http://localhost:3001/todos?id=eq.1"
Production Usage¶
In production, PostgREST runs as a sidecar in your app pod. Requests go through the Ingress:
The /api/rest prefix is stripped by nginx before reaching PostgREST. From your app's server-side code, use the internal URL for faster access:
// Server-side (same pod, no network hop)
const res = await fetch(`${process.env.POSTGREST_INTERNAL_URL}/todos`);
// Client-side (browser, goes through Ingress)
const res = await fetch(`${process.env.POSTGREST_URL}/todos`);
Controlling Access¶
By default, PostgREST uses the _anon role which has no table permissions. You control what's exposed in your migrations:
-- Expose read-only access
GRANT SELECT ON todos TO my_app_anon;
-- Expose full CRUD
GRANT ALL ON todos TO my_app_anon;
-- No grant = not accessible through PostgREST
Row-level security (RLS) policies are respected automatically.
Schema Reload¶
When you run a migration that adds or changes tables, PostgREST automatically detects the change (via PostgreSQL LISTEN/NOTIFY). You can also trigger a manual reload:
Object Storage¶
Using S3-Compatible Storage¶
Buckets defined in plattr.yaml are created automatically. Use any S3 SDK:
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({
endpoint: process.env.S3_ENDPOINT,
region: process.env.S3_REGION,
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY,
secretAccessKey: process.env.S3_SECRET_KEY,
},
forcePathStyle: true, // Required for MinIO (local)
});
await s3.send(new PutObjectCommand({
Bucket: process.env.S3_BUCKET_UPLOADS,
Key: 'photo.jpg',
Body: fileBuffer,
}));
Local MinIO Console¶
Open http://localhost:9001 in your browser. Login with minioadmin / minioadmin.
Public vs Private Buckets¶
storage:
enabled: true
buckets:
- name: uploads # Private — requires signed URLs
public: false
- name: assets # Public — accessible via direct URL
public: true
Redis¶
Local Development¶
When redis.enabled: true, a Redis 7 instance runs in the Kind cluster, accessible at redis://127.0.0.1:6379.
import { createClient } from 'redis';
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
await redis.set('key', 'value');
const value = await redis.get('key');
Reset Redis Data¶
Deletes the persistent volume and restarts Redis.
OpenSearch¶
Local Development¶
When search.enabled: true, OpenSearch 2.18 runs in the Kind cluster with security disabled for local convenience. The API is available at http://127.0.0.1:9200 and the Dashboards UI at http://127.0.0.1:5601.
import { Client } from '@opensearch-project/opensearch';
const client = new Client({ node: process.env.OPENSEARCH_URL });
// Create an index
await client.indices.create({ index: 'products' });
// Index a document
await client.index({
index: 'products',
body: { name: 'Widget', price: 9.99 },
});
// Search
const { body } = await client.search({
index: 'products',
body: { query: { match: { name: 'widget' } } },
});
OpenSearch Dashboards¶
Open http://localhost:5601 in your browser for the Dashboards UI (no login required locally).
Reset OpenSearch Data¶
Deletes the persistent volume and restarts OpenSearch.
Authentication¶
Local Development¶
Keycloak runs at http://localhost:8080 with a pre-configured realm matching your app name. A test user and OIDC client are provisioned automatically.
Admin console: http://localhost:8080/admin (credentials: admin / admin)
OIDC Integration¶
Use the standard OIDC flow with the provided env vars:
// Example: NextAuth.js configuration
import KeycloakProvider from 'next-auth/providers/keycloak';
export const authOptions = {
providers: [
KeycloakProvider({
clientId: process.env.AUTH_CLIENT_ID,
issuer: process.env.AUTH_ISSUER_URL,
}),
],
};
Identity Providers¶
Providers listed in plattr.yaml (google, github, saml, oidc) are noted for configuration but require Plattr-level OAuth credentials set up by your platform team.
Local Deploy Pipeline¶
Test your production container locally before pushing to CI:
This runs a full pipeline:
1. Tests — auto-detected test runner
2. Build — production container image via Dagger
3. Push — to the local registry (localhost:5050)
4. Security scan — Trivy scans for HIGH/CRITICAL vulnerabilities
5. Deploy — to the Kind cluster with port-forwarding
# Skip tests and scan for quick iteration
plattr deploy local --skip-tests --skip-scan
# Fail on vulnerabilities (CI-style)
plattr deploy local --fail-on-scan
# Remove the deployment
plattr undeploy local
Deploying to Production¶
CI/CD Pipeline¶
plattr init generates .github/workflows/plattr-deploy.yml which:
- Builds a production container image
- Pushes to ECR
- Updates the
ApplicationCRD with the newimageRef - The operator reconciles the change (rolling update)
Manual Build and Deploy¶
Environments¶
Plattr supports four environments:
| Environment | Namespace | Domain Pattern |
|---|---|---|
production |
production |
{app}.{baseDomain} |
staging |
staging |
{app}.staging.{baseDomain} |
uat |
uat |
{app}.uat.{baseDomain} |
preview |
preview-{app}-pr-{N} |
pr-{N}.{app}.preview.{baseDomain} |
Each environment gets its own database schema, storage buckets, and Keycloak realm — fully isolated.
Preview Environments¶
Every pull request can get an isolated preview environment with its own database, storage, and URL.
Local Preview¶
Remote Preview¶
Remote previews are created automatically when a PreviewEnvironment CRD is applied (typically from CI). List active previews:
Output:
PR App Phase URL Expires
42 my-app Running https://pr-42.my-app.preview.company.dev 2024-01-15T12:00:00Z
87 my-app Running https://pr-87.my-app.preview.company.dev 2024-01-16T08:00:00Z
Preview environments have a TTL (default 72 hours) and are automatically cleaned up when they expire.
Managing Environment Variables¶
Set Variables¶
# Set one or more variables
plattr env set API_KEY=abc123
plattr env set API_KEY=abc123 FEATURE_FLAG=true
# Target a specific environment
plattr env set --env staging API_KEY=staging-key
List Variables¶
Output shows both Plattr-managed and user-defined variables:
Plattr-managed:
DATABASE_URL = postgresql://...
DB_HOST = aurora-cluster.us-east-1.rds.amazonaws.com
S3_ENDPOINT = https://s3.us-east-1.amazonaws.com
...
User-defined:
API_KEY = abc123
FEATURE_FLAG = true
Remove Variables¶
Setting or unsetting variables triggers a deployment restart to pick up the changes.
Monitoring¶
Application Status¶
Shows phase (Pending, Provisioning, Running, Failed) and condition status for each subsystem.
Application Logs¶
# Recent logs
plattr logs
# Stream logs
plattr logs -f
# Last 100 lines
plattr logs --tail 100
# Staging environment
plattr logs --env staging
# Preview environment
plattr logs --pr 42
Logs include all containers in the pod (app + PostgREST sidecar if enabled).
Example: Next.js App from Scratch¶
# 1. Create Next.js app
npx create-next-app@latest my-app
cd my-app
# 2. Initialize plattr config
plattr init
# Choose: nextjs, enable database, enable storage, enable auth
# 3. Start local infrastructure
plattr dev
# 4. Source env vars and start dev server
source .plattr/my-app.env
npx next dev
# App on :3000, DB on :5432, Storage on :9000, Auth on :8080, REST API on :3001
# 5. Create a migration (example with Prisma)
npx prisma init
# Edit prisma/schema.prisma, then:
npx prisma migrate dev --name init
# 6. Grant PostgREST access to your tables
# In a migration file:
# GRANT SELECT, INSERT, UPDATE, DELETE ON todos TO my_app_anon;
# 7. Test the REST API
curl http://localhost:3001/todos
# 8. Run tests
plattr test
# 9. Test production container locally
plattr deploy local
# 10. Push to deploy
git add . && git commit -m "Initial app"
git push origin main
# CI builds, pushes image, operator deploys
# 11. Check status
plattr status
Example: Rails App from Scratch¶
# 1. Create Rails app
rails new my-rails-app --database=postgresql
cd my-rails-app
# 2. Initialize plattr config
plattr init
# Choose: rails, enable database
# 3. Update database.yml to use plattr env vars
# config/database.yml:
# default: &default
# adapter: postgresql
# url: <%= ENV['DATABASE_URL'] %>
# 4. Start local infrastructure
plattr dev
# 5. Source env vars and start dev server
source .plattr/my-rails-app.env
bin/rails server
# 6. Run migrations
plattr db migrate --engine raw
# 7. Push to deploy
git add . && git commit -m "Initial app"
git push origin main