feat: phase 6 production hardening + DB-to-DB transfer

Security:
- Graceful shutdown: http.Server + signal.NotifyContext, 10s drain,
  retention sweeper cancelled before pool close
- Scoped API keys (migration 00006): ingest/read/write/admin enforced in
  auth middleware; scope picker + badges in Settings UI; scopeless legacy
  keys act as admin. Read-only keys now get 403 on ingest (verified live)
- nginx: baseline security headers on all responses, CSP scoped to SPA;
  fixed add_header inheritance shadowing
- better-auth rate limit enabled unconditionally (was gated on NODE_ENV,
  which the shipped .env sets to development)
- Mailpit moved to docker-compose.dev.yml; prod compose cannot expose it
- setup.sh generates PRIMORA_ENCRYPTION_KEY

Reliability/ops:
- Retention: per-project retention_{events,audit,webhook}_days
  (migration 00007, 0=off) + hourly RetentionSweeper; edit UI in Projects
- GET /api/v1/metrics Prometheus exposition
- compose: healthchecks + restart policies everywhere, dragonfly pinned
  to v2.0.0, depends_on service_healthy
- backup.sh/restore.sh (pg_dump -Fc + storage volume); restore verified
  against scratch postgres+volume
- .dockerignore: exclude target dirs (build context was 4.7GB)

Correctness/testing:
- DBX transfer endpoint: POST /db-connections/:id/transfer — SQL or Redis
  target, 1000-row bound, server-side creds (verified live pg→dragonfly)
- Pagination audit: ListComponents capped at 500 (auto-registers)
- CI: goose up/reset/up job + compose e2e-smoke job (smoke-e2e.sh
  verified locally: signup, bootstrap, scoped keys, ingest, metrics)
- k6 load sanity script: 6650 reqs, ~217/s, 0 real failures, p95 221ms
  (429s = configured rate limiter working)

Docs: DEPLOYMENT_GUIDE.md, PRODUCTION_READINESS.md; verify script
repaired (stale refs + set -e arithmetic abort). ROADMAP: phase 2 +
phase 6 marked complete.

Authored By: TDvorak <[email protected]>

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-17 09:15:42 +02:00
1 parent 1b34dd43bb
commit 55edbf8b56
64 files changed
+2058 -165

No files matched your search

@@ -55,6 +55,8 @@ export type { DBRedisRequest } from './models/DBRedisRequest';
export type { DBRedisResponse } from './models/DBRedisResponse';
export type { DBSchemaContextResponse } from './models/DBSchemaContextResponse';
export type { DBTableListResponse } from './models/DBTableListResponse';
export type { DBTransferRequest } from './models/DBTransferRequest';
export type { DBTransferResponse } from './models/DBTransferResponse';
export type { DBXStatus } from './models/DBXStatus';
export type { DeployMarker } from './models/DeployMarker';
export type { Document } from './models/Document';
@@ -168,6 +170,8 @@ export { $DBRedisRequest } from './schemas/$DBRedisRequest';
export { $DBRedisResponse } from './schemas/$DBRedisResponse';
export { $DBSchemaContextResponse } from './schemas/$DBSchemaContextResponse';
export { $DBTableListResponse } from './schemas/$DBTableListResponse';
export { $DBTransferRequest } from './schemas/$DBTransferRequest';
export { $DBTransferResponse } from './schemas/$DBTransferResponse';
export { $DBXStatus } from './schemas/$DBXStatus';
export { $DeployMarker } from './schemas/$DeployMarker';
export { $Document } from './schemas/$Document';
@@ -7,6 +7,7 @@ export type ApiKey = {
project_id: string;
name: string;
prefix: string;
scopes?: Array<'ingest' | 'read' | 'write' | 'admin'>;
last_used_at?: string | null;
revoked_at?: string | null;
};
@@ -4,5 +4,9 @@
/* eslint-disable */
export type CreateApiKeyRequest = {
name: string;
/**
* Access scopes for the key. admin unlocks everything; write unlocks read + ingest + non-admin mutations; read and ingest unlock only themselves. Omitted means ["admin"] (full access, legacy behaviour).
*/
scopes?: Array<'ingest' | 'read' | 'write' | 'admin'>;
};
@@ -7,5 +7,6 @@ export type CreatedApiKey = {
prefix: string;
secret: string;
name: string;
scopes?: Array<'ingest' | 'read' | 'write' | 'admin'>;
};
@@ -0,0 +1,30 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DBTransferRequest = {
target_connection_id: string;
/**
* SELECT run against the source connection.
*/
query: string;
database?: string;
/**
* Destination table for SQL targets (schema-qualified allowed).
*/
target_table?: string;
/**
* Redis targets: SET key template with {column} placeholders, e.g. "user:{id}".
*/
key_pattern?: string;
/**
* Redis targets: column whose cell becomes the value. Omitted stores the whole row as JSON.
*/
value_column?: string;
/**
* Optional EX TTL applied to each Redis SET.
*/
ttl_seconds?: number;
limit?: number;
};
@@ -0,0 +1,11 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DBTransferResponse = {
transferred: number;
truncated: boolean;
source: string;
target: string;
};
@@ -9,5 +9,8 @@ export type Project = {
name: string;
description?: string | null;
membership_role?: string | null;
retention_events_days?: number;
retention_audit_days?: number;
retention_webhook_days?: number;
};
@@ -6,5 +6,17 @@ export type UpdateProjectRequest = {
name: string;
slug: string;
description?: string | null;
/**
* Telemetry event retention; 0 disables the sweep.
*/
retention_events_days?: number;
/**
* Audit log retention; 0 disables the sweep.
*/
retention_audit_days?: number;
/**
* Webhook delivery retention; 0 disables the sweep.
*/
retention_webhook_days?: number;
};
@@ -22,6 +22,12 @@ export const $ApiKey = {
type: 'string',
isRequired: true,
},
scopes: {
type: 'array',
contains: {
type: 'Enum',
},
},
last_used_at: {
type: 'string',
isNullable: true,
@@ -8,5 +8,11 @@ export const $CreateApiKeyRequest = {
type: 'string',
isRequired: true,
},
scopes: {
type: 'array',
contains: {
type: 'Enum',
},
},
},
} as const;
@@ -21,5 +21,11 @@ export const $CreatedApiKey = {
type: 'string',
isRequired: true,
},
scopes: {
type: 'array',
contains: {
type: 'Enum',
},
},
},
} as const;
@@ -0,0 +1,42 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $DBTransferRequest = {
properties: {
target_connection_id: {
type: 'string',
isRequired: true,
format: 'uuid',
},
query: {
type: 'string',
description: `SELECT run against the source connection.`,
isRequired: true,
},
database: {
type: 'string',
},
target_table: {
type: 'string',
description: `Destination table for SQL targets (schema-qualified allowed).`,
},
key_pattern: {
type: 'string',
description: `Redis targets: SET key template with {column} placeholders, e.g. "user:{id}".`,
},
value_column: {
type: 'string',
description: `Redis targets: column whose cell becomes the value. Omitted stores the whole row as JSON.`,
},
ttl_seconds: {
type: 'number',
description: `Optional EX TTL applied to each Redis SET.`,
},
limit: {
type: 'number',
maximum: 1000,
minimum: 1,
},
},
} as const;
@@ -0,0 +1,24 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $DBTransferResponse = {
properties: {
transferred: {
type: 'number',
isRequired: true,
},
truncated: {
type: 'boolean',
isRequired: true,
},
source: {
type: 'string',
isRequired: true,
},
target: {
type: 'string',
isRequired: true,
},
},
} as const;
@@ -30,5 +30,14 @@ export const $Project = {
type: 'string',
isNullable: true,
},
retention_events_days: {
type: 'number',
},
retention_audit_days: {
type: 'number',
},
retention_webhook_days: {
type: 'number',
},
},
} as const;
@@ -16,5 +16,20 @@ export const $UpdateProjectRequest = {
type: 'string',
isNullable: true,
},
retention_events_days: {
type: 'number',
description: `Telemetry event retention; 0 disables the sweep.`,
maximum: 3650,
},
retention_audit_days: {
type: 'number',
description: `Audit log retention; 0 disables the sweep.`,
maximum: 3650,
},
retention_webhook_days: {
type: 'number',
description: `Webhook delivery retention; 0 disables the sweep.`,
maximum: 3650,
},
},
} as const;
@@ -15,6 +15,8 @@ import type { DBRedisRequest } from '../models/DBRedisRequest';
import type { DBRedisResponse } from '../models/DBRedisResponse';
import type { DBSchemaContextResponse } from '../models/DBSchemaContextResponse';
import type { DBTableListResponse } from '../models/DBTableListResponse';
import type { DBTransferRequest } from '../models/DBTransferRequest';
import type { DBTransferResponse } from '../models/DBTransferResponse';
import type { DBXStatus } from '../models/DBXStatus';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
@@ -333,4 +335,33 @@ export class DatabasesService {
},
});
}
/**
* Move query results from this connection to another saved connection
* @returns DBTransferResponse Transfer result
* @throws ApiError
*/
public static transferDbRows({
projectId,
connectionId,
requestBody,
}: {
projectId: string,
connectionId: string,
requestBody: DBTransferRequest,
}): CancelablePromise<DBTransferResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/projects/{projectID}/db-connections/{connectionID}/transfer',
path: {
'projectID': projectId,
'connectionID': connectionId,
},
body: requestBody,
mediaType: 'application/json',
errors: {
404: `Error response`,
502: `Error response`,
},
});
}
}
@@ -28,4 +28,15 @@ export class HealthService {
url: '/health/readiness',
});
}
/**
* Prometheus text exposition of platform HTTP metrics
* @returns string Prometheus metrics
* @throws ApiError
*/
public static getMetrics(): CancelablePromise<string> {
return __request(OpenAPI, {
method: 'GET',
url: '/metrics',
});
}
}