const crypto = require('crypto'); const express = require('express'); const session = require('express-session'); const { Issuer, generators } = require('openid-client'); const { Pool } = require('pg'); const app = express(); const port = Number(process.env.PORT || 3000); const appUrl = String(process.env.APP_URL || process.env.COOLIFY_URL || '').replace(/\/$/, ''); const issuerUrl = process.env.KEYCLOAK_ISSUER; const clientId = process.env.KEYCLOAK_CLIENT_ID; const clientSecret = process.env.KEYCLOAK_CLIENT_SECRET; const databaseUrl = process.env.DATABASE_URL; const tenantId = process.env.TENANT_ID || 'demo-tenant'; const tenantMode = process.env.TENANT_DB_MODE || 'unknown'; const tenantSchema = process.env.TENANT_DB_SCHEMA || ''; for (const [key, value] of Object.entries({ APP_URL: appUrl, KEYCLOAK_ISSUER: issuerUrl, KEYCLOAK_CLIENT_ID: clientId, KEYCLOAK_CLIENT_SECRET: clientSecret, DATABASE_URL: databaseUrl })) { if (!value) throw new Error(`${key} is required`); } const pool = new Pool({ connectionString: databaseUrl }); let oidcClient; function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function waitForDatabase() { let lastError; for (let attempt = 1; attempt <= 45; attempt++) { try { await pool.query('SELECT 1'); await pool.query(` CREATE TABLE IF NOT EXISTS tenant_visits ( id BIGSERIAL PRIMARY KEY, tenant_id TEXT NOT NULL, subject TEXT NOT NULL, username TEXT, visited_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `); return; } catch (error) { lastError = error; console.log(`database not ready (attempt ${attempt}/45): ${error.message}`); await sleep(2000); } } throw lastError; } function escapeHtml(value) { return String(value ?? '') .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } function asyncRoute(fn) { return (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next); } async function main() { await waitForDatabase(); const issuer = await Issuer.discover(issuerUrl); oidcClient = new issuer.Client({ client_id: clientId, client_secret: clientSecret, redirect_uris: [`${appUrl}/callback`], response_types: ['code'], }); app.set('trust proxy', 1); app.use(session({ secret: process.env.SESSION_SECRET || crypto.randomBytes(32).toString('hex'), resave: false, saveUninitialized: false, cookie: { secure: true, httpOnly: true, sameSite: 'lax', maxAge: 8 * 60 * 60 * 1000, }, })); app.get('/health', asyncRoute(async (_req, res) => { await pool.query('SELECT 1'); res.status(200).send('ok'); })); app.get('/login', (req, res) => { const codeVerifier = generators.codeVerifier(); const codeChallenge = generators.codeChallenge(codeVerifier); const state = generators.state(); req.session.codeVerifier = codeVerifier; req.session.state = state; const url = oidcClient.authorizationUrl({ scope: 'openid profile email', code_challenge: codeChallenge, code_challenge_method: 'S256', state, }); res.redirect(url); }); app.get('/callback', asyncRoute(async (req, res) => { const params = oidcClient.callbackParams(req); const tokenSet = await oidcClient.callback(`${appUrl}/callback`, params, { code_verifier: req.session.codeVerifier, state: req.session.state, }); req.session.user = tokenSet.claims(); delete req.session.codeVerifier; delete req.session.state; res.redirect('/'); })); app.get('/logout', (req, res) => { req.session.destroy(() => res.redirect('/logged-out')); }); app.get('/logged-out', (_req, res) => { res.type('html').send('

Signed out

Sign in again

'); }); app.get('/', asyncRoute(async (req, res) => { if (!req.session.user) return res.redirect('/login'); const user = req.session.user; const username = user.preferred_username || user.email || user.name || user.sub; await pool.query( 'INSERT INTO tenant_visits (tenant_id, subject, username) VALUES ($1, $2, $3)', [tenantId, user.sub, username] ); const countResult = await pool.query( 'SELECT COUNT(*)::bigint AS count FROM tenant_visits WHERE tenant_id = $1', [tenantId] ); const lastResult = await pool.query( 'SELECT visited_at FROM tenant_visits WHERE tenant_id = $1 ORDER BY id DESC LIMIT 1', [tenantId] ); res.type('html').send(` Arrowstack Tenant DB Demo

Authenticated + persisted ✅

This page required Keycloak login and then wrote your visit to PostgreSQL provisioned by Arrowstack.

User${escapeHtml(username)}
Tenant${escapeHtml(tenantId)}
DB mode${escapeHtml(tenantMode)}
Schema${escapeHtml(tenantSchema || '(default)')}
Persisted visits${escapeHtml(countResult.rows[0].count)}
Last write${escapeHtml(lastResult.rows[0]?.visited_at || '')}
Keycloak client${escapeHtml(clientId)}

Record another visit · Local sign out

`); })); app.use((error, _req, res, _next) => { console.error(error); res.status(500).type('text').send('Application error'); }); app.listen(port, '0.0.0.0', () => { console.log(`tenant-db-keycloak-demo listening on ${port}`); }); } main().catch((error) => { console.error(error); process.exit(1); });