Scaffold dd0c/drift SaaS backend: Fastify, RLS, ingestion, dashboard API

- Fastify server with Zod validation, pino logging, CORS/helmet
- Drift report ingestion endpoint with nonce replay prevention
- Dashboard API: stacks list, drift history, report detail, summary stats
- PostgreSQL schema with RLS: tenants, users, agent_keys, drift_reports, remediation_actions
- withTenant() helper for safe connection pool tenant context management
- Config via Zod-validated env vars
This commit is contained in:
2026-03-01 02:45:33 +00:00
parent 31cb36fb77
commit e67cef518e
9 changed files with 486 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
import pg from 'pg';
export function createPool(connectionString: string): pg.Pool {
return new pg.Pool({
connectionString,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
});
}
/**
* Set tenant context on a pooled connection for RLS.
* MUST be called before any query in a multi-tenant context.
* MUST be cleared when returning the connection to the pool.
*/
export async function setTenantContext(client: pg.PoolClient, tenantId: string): Promise<void> {
await client.query('SET LOCAL app.tenant_id = $1', [tenantId]);
}
/**
* Clear tenant context — call this in a finally block before releasing the client.
*/
export async function clearTenantContext(client: pg.PoolClient): Promise<void> {
await client.query('RESET app.tenant_id');
}
/**
* Execute a query within a tenant-scoped transaction.
* Handles SET LOCAL + RESET automatically to prevent RLS leakage via connection pooling.
*/
export async function withTenant<T>(
pool: pg.Pool,
tenantId: string,
fn: (client: pg.PoolClient) => Promise<T>,
): Promise<T> {
const client = await pool.connect();
try {
await client.query('BEGIN');
await setTenantContext(client, tenantId);
const result = await fn(client);
await client.query('COMMIT');
return result;
} catch (err) {
await client.query('ROLLBACK');
throw err;
} finally {
await clearTenantContext(client);
client.release();
}
}

View File

@@ -0,0 +1,10 @@
import Redis from 'ioredis';
export function createRedis(url: string): Redis {
return new Redis(url, {
maxRetriesPerRequest: 3,
retryStrategy(times) {
return Math.min(times * 200, 3000);
},
});
}