Skip to content

Add PostgreSQL persistence layer#16

Merged
LahkLeKey merged 1 commit into
mainfrom
feature/postgres-persistence
Jul 12, 2026
Merged

Add PostgreSQL persistence layer#16
LahkLeKey merged 1 commit into
mainfrom
feature/postgres-persistence

Conversation

@LahkLeKey

Copy link
Copy Markdown
Owner

Closes: #15

Summary

Implements persistent storage using PostgreSQL for Kloak deployments. The server auto-detects DATABASE_URL and uses PostgreSQL when available, falling back to in-memory storage for local development.

Architecture

  • SqlDeploymentRepository — Implements DeploymentRepository interface with JSONB support for complex objects
  • Schema — 5 normalized tables with proper indexes for query performance
  • Migration — Automatic schema initialization on server startup
  • Server — Smart detection of DATABASE_URL to choose repository implementation

Changes Made

  1. Created src/database/ module with schema, repository, and migrations
  2. Updated server.ts to support both in-memory and PostgreSQL repositories
  3. Fixed 5 import statements to use explicit .ts extensions (ESM requirement)
  4. Created package.json with postgres (15KB, pure JS, zero node-gyp)
  5. Created .gitignore to exclude node_modules and other build artifacts

Testing

npm test
# Output: 10 passing tests, 0 failing

All existing tests pass, and the database module compiles without errors.

Deployment

Local Development (in-memory)

npm run dev
# Server uses in-memory repository

Local Development (PostgreSQL)

docker run --name kloak-postgres -e POSTGRES_DB=kloak -e POSTGRES_PASSWORD=dev -p 5432:5432 -d postgres:16
DATABASE_URL=postgresql://postgres:dev@localhost/kloak npm run dev
# Server runs migrations and uses PostgreSQL repository

fly.io Deployment

fly postgres create --name kloak-db
fly postgres attach kloak-db --app kloak
# DATABASE_URL injected automatically; migrations run on app start

Files Modified

  • package.json (created)
  • .gitignore (created)
  • src/database/schema.sql (created)
  • src/database/sql-repository.ts (created)
  • src/database/migrate.ts (created)
  • src/database/index.ts (created)
  • src/database/README.md (created)
  • src/admin-ui/server.ts (updated)
  • src/admin-ui/README.md (updated)
  • src/sync/index.ts (import fixes)
  • src/infrastructure/index.test.ts (import fix)
  • src/sync/index.test.ts (import fix)

- SqlDeploymentRepository for persistent storage
- Database schema with tables for deployments, versions, state, runs, audit
- Migration runner (src/database/migrate.ts)
- Server auto-detects DATABASE_URL and uses PostgreSQL or in-memory
- Fix import statements to use explicit .ts extensions
- npm install postgres dependency
Copilot AI review requested due to automatic review settings July 12, 2026 02:13
@LahkLeKey LahkLeKey self-assigned this Jul 12, 2026
@LahkLeKey
LahkLeKey merged commit 1bcfcfb into main Jul 12, 2026
@LahkLeKey
LahkLeKey deleted the feature/postgres-persistence branch July 12, 2026 02:13

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an optional PostgreSQL-backed persistence layer for deployments, intended to be auto-selected via DATABASE_URL with an in-memory fallback for local/dev usage.

Changes:

  • Introduces a new src/database/ module (schema, migration runner, SQL repository, docs).
  • Updates the admin UI server to choose PostgreSQL vs in-memory repository based on DATABASE_URL and run migrations on startup.
  • Adds an npm package setup (with postgres dependency) and updates several ESM imports to use explicit .ts extensions.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
package.json Adds npm scripts and the postgres dependency for running/tests/migrations.
.gitignore Ignores Node build artifacts and environment files.
src/database/schema.sql Defines initial PostgreSQL schema + indexes for persistence.
src/database/sql-repository.ts Implements DeploymentRepository using PostgreSQL queries.
src/database/migrate.ts Adds a migration runner that initializes the schema.
src/database/index.ts Exposes the database module’s public API.
src/database/README.md Documents local Postgres + fly.io setup.
src/admin-ui/server.ts Auto-detects DATABASE_URL and selects repository implementation.
src/admin-ui/README.md Updates server startup docs for in-memory vs Postgres mode.
src/sync/index.ts Updates ESM imports/exports to include explicit .ts extensions.
src/infrastructure/index.test.ts Updates ESM import to include explicit .ts extension.
src/sync/index.test.ts Updates ESM import to include explicit .ts extension.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/database/schema.sql
Comment on lines +24 to +28
CREATE TABLE IF NOT EXISTS desired_state_snapshots (
id UUID PRIMARY KEY,
version_id UUID NOT NULL,
keycloak_config JSONB NOT NULL,
auth_providers JSONB NOT NULL,
Comment on lines +119 to +127
async recordDesiredState(snapshot: DesiredStateSnapshot): Promise<void> {
await this.sql`
INSERT INTO desired_state_snapshots
(id, version_id, keycloak_config, auth_providers, created_at)
VALUES
(${snapshot.id}, ${snapshot.versionId},
${JSON.stringify(snapshot.keycloakConfig)},
${JSON.stringify(snapshot.authProviders)},
${snapshot.createdAt})
Comment on lines +94 to +105
async saveDeployment(deployment: Deployment): Promise<void> {
await this.sql`
INSERT INTO deployments
(id, customer_id, name, target, status, created_at, updated_at)
VALUES
(${deployment.id}, ${deployment.customerId}, ${deployment.name},
${JSON.stringify(deployment.target)}, ${deployment.status},
${deployment.createdAt}, ${deployment.updatedAt})
ON CONFLICT (id) DO UPDATE SET
status = EXCLUDED.status,
updated_at = EXCLUDED.updated_at
`;
Comment on lines +132 to +140
async recordReconciliationRun(run: ReconciliationRun): Promise<void> {
await this.sql`
INSERT INTO reconciliation_runs
(id, deployment_id, started_at, completed_at, status, drift_findings, error)
VALUES
(${run.id}, ${run.deploymentId}, ${run.startedAt},
${run.completedAt ?? null}, ${run.status},
${run.driftFindings ? JSON.stringify(run.driftFindings) : null},
${run.error ?? null})
Comment on lines +145 to +152
async appendAuditEvent(event: AuditEvent): Promise<void> {
await this.sql`
INSERT INTO audit_events
(id, deployment_id, event_type, data, created_at)
VALUES
(${event.id}, ${event.deploymentId}, ${event.eventType},
${event.data ? JSON.stringify(event.data) : null}, ${event.createdAt})
`;
Comment thread src/database/migrate.ts
Comment on lines +35 to +41
if (import.meta.url === `file://${process.argv[1]}`) {
const connectionString = process.env.DATABASE_URL || 'postgresql://localhost/kloak';
runMigrations(connectionString).catch((error) => {
console.error('Migration failed:', error);
process.exit(1);
});
}
Comment on lines +108 to +116
async saveDeploymentVersion(version: DeploymentVersion): Promise<void> {
await this.sql`
INSERT INTO deployment_versions
(id, deployment_id, number, created_by, notes, created_at)
VALUES
(${version.id}, ${version.deploymentId}, ${version.number},
${version.createdBy}, ${version.notes ?? null}, ${version.createdAt})
ON CONFLICT (deployment_id, number) DO NOTHING
`;
Comment thread package.json
Comment on lines +9 to +13
"scripts": {
"test": "node --experimental-transform-types --test src/**/*.test.ts",
"dev": "PORT=3000 node --experimental-transform-types src/admin-ui/cli.ts",
"migrate": "node --experimental-transform-types src/database/migrate.ts"
},
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants