This document describes the security architecture, data flows, threat mitigations, and compliance posture of Databricks Forge. It is intended for security reviewers, architects, and compliance teams evaluating the application prior to deployment.
- Application Overview
- Deployment Model
- Authentication and Authorization
- Network Architecture
- Data Classification and Flow
- LLM Security (Prompt Injection and AI Safety)
- Input Validation and Injection Prevention
- Secret Management
- HTTP Security Headers
- Error Handling and Information Leakage
- Logging and Auditability
- Resilience and Timeout Protection
- Container and Build Security
- API Surface and Threat Model
- Data Privacy and GDPR Considerations
- Known Limitations and Recommendations
Databricks Forge is a web application that discovers data-driven use cases from Unity Catalog metadata. Users configure a business context, point at their Unity Catalog catalogs/schemas, and the application generates scored, categorised use cases with optional SQL code.
Key architectural principle: The application reads only metadata (table names, column names, data types, foreign keys) from Unity Catalog. No row-level business data is accessed in the default configuration. An optional "sample data" feature can be enabled per-run, which is documented separately below.
graph LR
subgraph browser [User Browser]
UI[Next.js Frontend]
end
subgraph dbxApp [Databricks App Container]
API[Next.js API Routes]
Engine[Pipeline Engine]
end
subgraph dbxPlatform [Databricks Platform]
Proxy[Apps Reverse Proxy]
Warehouse[SQL Warehouse]
ModelServing[Model Serving]
Lakebase[Lakebase Postgres]
UC[Unity Catalog]
Workspace[Workspace API]
end
UI -->|HTTPS| Proxy
Proxy -->|Auth headers| API
API --> Engine
Engine -->|SQL Statement API| Warehouse
Warehouse -->|information_schema| UC
Engine -->|Chat Completions API| ModelServing
Engine -->|Prisma| Lakebase
Engine -->|Notebook export| Workspace
The application is designed exclusively for deployment as a Databricks App, running inside the customer's Databricks workspace boundary.
graph TB
subgraph customerVPC [Customer VPC / Network Boundary]
subgraph dbxWorkspace [Databricks Workspace]
direction TB
AppsProxy[Apps Reverse Proxy<br>TLS termination + user auth]
AppContainer[App Container<br>Node.js 18 Alpine]
SQLWarehouse[SQL Warehouse]
LakebaseDB[Lakebase PostgreSQL]
ModelEndpoint[Model Serving Endpoint]
end
end
Internet[User Browser] -->|HTTPS| AppsProxy
AppsProxy -->|x-forwarded-access-token| AppContainer
AppContainer -->|Statement Execution API| SQLWarehouse
AppContainer -->|Prisma TCP| LakebaseDB
AppContainer -->|Chat Completions API| ModelEndpoint
| Property | Value |
|---|---|
| Runtime | Node.js 18 (Alpine Linux) |
| Framework | Next.js 16 (standalone output) |
| Orchestration | Databricks Apps (managed container) |
| Network egress | None -- all APIs are workspace-internal |
| Public internet access | Not required by the application |
| Scaling | Single-instance (Databricks Apps default) |
env:
- name: DATABRICKS_WAREHOUSE_ID
valueFrom: sql-warehouse # Bound SQL Warehouse resource
- name: DATABRICKS_SERVING_ENDPOINT
valueFrom: serving-endpoint # Bound Model Serving endpointThe app uses on-behalf-of (OBO) user authorization for SQL queries and (optionally) Genie Space management. Configure these scopes in the Databricks App UI (Configure → +Add Scope):
| Scope | Purpose |
|---|---|
sql |
SQL Statement Execution API (metadata queries, generated SQL, health check) |
catalog.tables:read |
Read tables in Unity Catalog |
catalog.schemas:read |
Read schemas in Unity Catalog |
catalog.catalogs:read |
Read catalogs in Unity Catalog |
files.files |
Manage files and directories (notebook export) |
dashboards.genie |
Manage Genie Spaces (create, update, trash) |
iam.access-control:read |
(default -- auto-included) Access control resolution |
iam.current-user:read |
(default -- auto-included) User identity from proxy headers |
Model Serving and Workspace APIs use app authorization (service principal). Genie Space management defaults to user OBO auth (so the user's UC permissions apply to referenced tables) but can be switched to service principal in Settings > Genie Engine > Deploy Authentication.
The DATABRICKS_HOST, DATABRICKS_CLIENT_ID, and DATABRICKS_CLIENT_SECRET
are automatically injected by the Databricks Apps runtime. Runtime Lakebase
access defaults to native Postgres password auth against the pooler endpoint
(LAKEBASE_AUTH_MODE=native_password), with startup-generated endpoint metadata
(LAKEBASE_ENDPOINT_NAME, LAKEBASE_POOLER_HOST, LAKEBASE_USERNAME) from
scripts/provision-lakebase.mjs. OAuth runtime mode remains available as an
explicit deploy override.
No credentials are hardcoded or bundled with the application image.
flowchart TD
A[Incoming Request] --> B{x-forwarded-access-token<br>header present?}
B -->|Yes| C[User Authorization<br>On-behalf-of-user OAuth]
B -->|No| D{DATABRICKS_TOKEN<br>env var set?}
D -->|Yes| E[PAT Authentication<br>Local development only]
D -->|No| F{CLIENT_ID +<br>CLIENT_SECRET set?}
F -->|Yes| G[OAuth M2M<br>Service Principal]
F -->|No| H[Error: No credentials]
| Mode | When Used | UC Permissions |
|---|---|---|
| User authorization | Databricks App with user-auth scopes | Follow the logged-in user |
| PAT | Local development (DATABRICKS_TOKEN) |
Follow the token owner |
| OAuth M2M | Service principal fallback / background tasks | Follow the service principal |
| Header | Purpose |
|---|---|
x-forwarded-access-token |
User's OAuth access token (injected by Apps proxy) |
x-forwarded-email |
User's email address |
x-forwarded-preferred-username |
Fallback username |
The application enforces per-user isolation at the application layer (team-shared opt-in sharing) on top of the Databricks platform's catalog and warehouse permissions.
-
Unity Catalog permissions: Metadata queries run as the authenticated user (or service principal). The user can only see catalogs, schemas, and tables they have
USEorSELECTpermissions on. -
SQL Warehouse access: The user must have
CAN USEon the bound SQL Warehouse. -
Workspace access: Notebook export uses the logged-in user's OBO token (
getHeaderswithfiles.filesscope) so notebooks are created under the user's identity and ownership. Falls back to service principal when no request context is available. -
Model Serving: LLM inference uses service principal credentials (
getAppHeaders) because these operations are performed by the app. -
Genie Space management: Defaults to user OBO auth (
getHeaderswithdashboards.geniescope) so spaces are created under the user's identity and inherit their UC table permissions. Can be switched to service principal in Settings. The auth mode used to create each space is persisted and reused for subsequent updates and deletions. -
Lakebase (per-user isolation): Every root table (
ForgeRun,ForgeEnvironmentScan,ForgeGenieSpace,ForgeMetadataGenieSpace,ForgeSpaceBenchmarkRun,ForgeSpaceHealthScore,ForgeDemoSession,ForgeCommentJob,ForgeConnection,ForgeFabricScan,ForgeFabricMigration,ForgeStrategyDocument,ForgeDocument,ForgeQualityMetric) carries a NOT NULLowner_emailcolumn. List, detail, update, delete, and export paths apply a single visibility rule:WHERE owner_email = $user OR id IN (SELECT resource_id FROM forge_resource_acl WHERE resource_type = $type AND viewer_email = $user)
Owners can opt-in share resources via
ForgeResourceAclwithvieworeditpermissions (delete and re-share remain owner-only). Outcome maps, benchmarks, prompt templates, and metadata cache stay global by design. -
Vector search:
forge_embeddingsis filtered through accessible parent ids (run/scan/source) at query time -- callers passuserEmailand the reader resolves accessible ids vialib/lakebase/acl.ts. Catalog kinds (outcome_map,benchmark_context, skills, industry KPIs) remain global. -
Auth seam:
lib/auth/route-user.tsexposesrequireUser()andgetUserOrNull(). Next.js 16's root proxy (proxy.ts, the renamed middleware convention) enforces 401 on every/api/**route that lacks identity. Server Components callrequireUser()directly. Local dev supports a?as_user=query param override gated toNODE_ENV !== "production"for end-to-end isolation testing. -
Per-user fairness & quotas: Per-user active-resource caps (configurable via env, default 1 for pipelines/scans/demo engines, 2 for Genie deploys) are enforced at every fire-and-forget kickoff. Pipelines exceeding the cap are queued (
status='queued') and promoted by a process-local scheduler (lib/pipeline/scheduler.ts) when capacity opens up. The pool rate limiter tracks per-user inflight calls per endpoint to support fair-share scheduling. -
FORGE_USER_ISOLATIONfeature flag: gates the UI sharing dialog and per-user quota enforcement. Data-layer filtering byowner_emailis unconditional and not affected by the flag.
The application is stateless. There are no sessions, cookies, or server-side session stores. Each request is authenticated independently via the forwarded token or environment credentials.
Service principal OAuth tokens are cached in-memory with a 60-second expiry buffer. The cache is process-local and not shared across instances.
| Destination | Protocol | Port | Purpose |
|---|---|---|---|
| SQL Warehouse | HTTPS | 443 | Metadata queries, generated SQL execution |
| Model Serving | HTTPS | 443 | LLM inference (chat completions API) |
| Lakebase | TLS/TCP | 5432 | Pipeline run persistence (Prisma ORM) |
| Workspace API | HTTPS | 443 | Notebook export only |
| OIDC endpoint | HTTPS | 443 | OAuth M2M token exchange |
All connections remain within the Databricks workspace boundary. The application makes zero calls to public internet endpoints.
- All Databricks API calls use HTTPS (TLS 1.2+).
- TLS termination for inbound traffic is handled by the Databricks Apps reverse proxy.
- Lakebase connections use TLS via runtime-generated Postgres URLs.
| Data Category | Classification | Examples |
|---|---|---|
| User configuration | Internal | Business name, strategic goals, priorities |
| UC metadata | Internal / Confidential | Table names, column names, data types, FKs |
| LLM-generated content | Internal | Business context, use cases, SQL code |
| Sample row data (opt-in) | Confidential / Restricted | Actual table values (truncated to 60 chars) |
flowchart LR
subgraph inputs [Inputs]
UserConfig[User Config<br>business name, goals, priorities]
UCMetadata[Unity Catalog<br>information_schema]
SampleData[Sample Rows<br>SELECT * LIMIT N<br>opt-in only]
end
subgraph processing [Processing - Pipeline Engine]
Step1[Step 1: Business Context<br>LLM generates context]
Step3[Step 3: Table Filtering<br>LLM classifies tables]
Step4[Step 4: Use Case Gen<br>LLM generates use cases]
Step5[Step 5: Domain Clustering<br>LLM assigns domains]
Step6[Step 6: Scoring<br>LLM scores + dedup]
Step7[Step 7: SQL Generation<br>LLM writes SQL]
end
subgraph persistence [Persistence]
Lakebase2[Lakebase<br>runs, use cases, exports]
end
subgraph outputs [Outputs]
Excel[Excel export]
PDF[PDF export]
PPTX[PPTX export]
Notebooks[SQL Notebooks<br>via Workspace API]
end
UserConfig --> Step1
UCMetadata --> Step3
UCMetadata --> Step4
SampleData -.->|optional| Step7
Step1 --> Step3 --> Step4 --> Step5 --> Step6 --> Step7
Step7 --> Lakebase2
Lakebase2 --> Excel
Lakebase2 --> PDF
Lakebase2 --> PPTX
Lakebase2 --> Notebooks
| Pipeline Step | Data Sent | Contains Row Data? |
|---|---|---|
| Business Context | Business name, industry | No |
| Table Filtering | Table FQNs, types, comments + business context | No |
| Use Case Generation | Schema markdown (table/column names, types) + FK relationships | No |
| Domain Clustering | Use case names, statements (LLM-generated) | No |
| Scoring | Use case summaries (LLM-generated) | No |
| SQL Generation | Schema markdown + FK relationships + sample rows (if enabled) | Only if opt-in |
| SQL Fix | Original SQL + error message + schema | No |
When sampleRowsPerTable > 0 (configurable in Settings, range 0-50):
SELECT * FROM <table> LIMIT Nis executed for each table referenced by a use case during SQL generation.- Values are truncated to 60 characters.
- Sample data is formatted as markdown and injected into the SQL-generation prompt only.
- Sample data is NOT persisted -- it exists only in-memory during the generation step and is discarded after the LLM call.
- Sample data is NOT exported -- it does not appear in Excel, PDF, PPTX, or notebook outputs.
| Data | Storage | Retention |
|---|---|---|
| Pipeline runs | Lakebase (forge_runs) | Until user deletes via UI |
| Use cases | Lakebase (forge_use_cases) | Cascade-deleted with run |
| Export records | Lakebase (forge_exports) | Cascade-deleted with run |
| Exported files (Excel/PDF/PPTX) | In-memory only | Not persisted on disk |
| Exported notebooks | Databricks Workspace | Governed by Workspace retention |
All LLM calls are executed via direct REST calls to the Databricks Model
Serving chat completions API (/serving-endpoints/{endpoint}/invocations).
The application does not call external LLM APIs (OpenAI, Anthropic, etc.)
directly. The model endpoint is workspace-internal. The SQL Warehouse is used
only for metadata queries and generated SQL execution -- not for LLM inference.
sequenceDiagram
participant App as Pipeline Engine
participant MS as Model Serving
App->>MS: POST /serving-endpoints/{endpoint}/invocations
Note right of App: Chat completions format (system + user messages)
MS-->>App: JSON response with content + token usage
| Control | Implementation |
|---|---|
| Delimiter wrapping | User-supplied text is wrapped in ---BEGIN USER DATA--- / ---END USER DATA--- markers before injection into prompts |
| Marker stripping | Existing delimiter markers in user input are stripped to prevent delimiter escape attacks |
| User variable identification | A whitelist (USER_INPUT_VARIABLES) identifies which template variables contain user input and require sanitisation |
| System/user separation | Prompts use chat completions format with separate system and user messages, providing structural isolation between instructions and user data |
| JSON mode | Most pipeline steps use response_format: json_object which constrains LLM output to valid JSON, reducing attack surface |
| Output validation | LLM JSON outputs are parsed and validated with Zod schemas before use; malformed items are dropped |
| Structured output formats | Prompts request specific JSON array output formats, reducing free-text attack surface |
Each LLM response that includes an honesty check is scored (0.0-1.0). The agent logs a warning when the score falls below 0.3, indicating the LLM may be uncertain about output quality. This provides a signal for detecting adversarial or low-quality responses.
- The model endpoint is configured per-run (
aiModelfield). - The endpoint must be a Databricks Model Serving endpoint accessible from the workspace.
- Model selection is restricted to endpoints the service principal or user
has
CAN QUERYpermissions on.
flowchart LR
UserInput[User Input<br>catalog/schema names] --> Validate{validateIdentifier<br>regex: a-zA-Z0-9_-}
Validate -->|Pass| Interpolate[Safe SQL interpolation]
Validate -->|Fail| Reject[400 Bad Request]
| Control | Detail |
|---|---|
| Identifier validation | All catalog/schema names pass through validateIdentifier() which enforces /^[a-zA-Z0-9_\-]+$/ |
| Length limits | Identifiers capped at 255 characters |
| UUID validation | All runId parameters validated against UUID regex before database queries |
| Zod schemas | API request bodies validated with Zod: CreateRunSchema (field lengths, types, ranges), MetadataQuerySchema (enum types) |
| Safe body parsing | safeParseBody() wraps JSON parse + Zod validation, returning structured errors |
| Route | Validation Applied |
|---|---|
POST /api/runs |
CreateRunSchema (Zod), field length limits |
GET /api/runs |
limit (1-200), offset (>=0) |
GET /api/runs/[runId] |
isValidUUID(runId) |
DELETE /api/runs/[runId] |
isValidUUID(runId) |
POST /api/runs/[runId]/execute |
isValidUUID(runId) |
GET /api/metadata |
validateIdentifier() for catalog/schema |
GET /api/health |
No user input |
All LLM responses are validated before use:
| Output Type | Validation |
|---|---|
| Business context JSON | parseJSONResponse() with try/catch fallback to defaults |
| Score items | ScoreItemSchema (Zod) -- validates no, priority_score, feasibility_score, impact_score, overall_score |
| Dedup items | DedupItemSchema (Zod) -- validates no, action, reason |
| Domain assignments | DomainAssignmentSchema (Zod) -- validates no, domain |
| Subdomain assignments | SubdomainAssignmentSchema (Zod) -- validates no, subdomain |
| Calibration items | CalibrationItemSchema (Zod) -- validates no, overall_score |
| Cross-domain dedup | CrossDomainDedupItemSchema (Zod) -- validates no, duplicate_of, reason |
| SQL output | Structural validation (keyword check, table reference check, column existence check) |
| CSV responses | Column count tolerance (+-2), try/catch with empty fallback |
Invalid items are logged and dropped; they do not crash the pipeline.
| Secret | Source | Scope |
|---|---|---|
DATABRICKS_HOST |
Databricks Apps runtime | Platform-injected |
DATABRICKS_CLIENT_ID |
Databricks Apps runtime | Platform-injected |
DATABRICKS_CLIENT_SECRET |
Databricks Apps runtime | Platform-injected |
DATABRICKS_WAREHOUSE_ID |
app.yaml resource binding |
Platform-injected |
LAKEBASE_ENDPOINT_NAME |
Auto-generated at startup | Lakebase endpoint resource name |
LAKEBASE_POOLER_HOST |
Auto-generated at startup | Lakebase runtime pooler host |
LAKEBASE_USERNAME |
Auto-generated at startup | Lakebase runtime username |
LAKEBASE_AUTH_MODE |
Deploy/startup configuration | Runtime DB auth mode |
LAKEBASE_NATIVE_USER |
Deploy/startup configuration | Native Postgres runtime role |
LAKEBASE_NATIVE_PASSWORD |
Deploy rotation or startup fallback | Native Postgres runtime password |
DATABASE_URL |
.env.local (local dev only) |
Local fallback connection string |
DATABRICKS_TOKEN |
.env.local (local dev only) |
Developer machine |
| Control | Implementation |
|---|---|
| No hardcoded secrets | All credentials from environment variables |
| gitignore | .env* files excluded (except .env.local.example) |
| No secrets in Docker image | Multi-stage build; env vars provided at runtime |
| Token caching | OAuth tokens cached in-memory with 60s expiry buffer; no disk persistence |
| Secret rotation | Native password rotation is controlled via deploy.sh flags (--rotate-lakebase-native-password) with explicit deploy audit trail; OAuth fallback mode remains available |
Applied to all routes via next.config.ts:
| Header | Value | Purpose |
|---|---|---|
X-Frame-Options |
DENY |
Prevent clickjacking |
X-Content-Type-Options |
nosniff |
Prevent MIME-type sniffing |
Referrer-Policy |
strict-origin-when-cross-origin |
Limit referrer information |
Permissions-Policy |
camera=(), microphone=(), geolocation=() |
Disable unnecessary browser APIs |
X-Powered-By |
(removed) | poweredByHeader: false hides server technology |
reactStrictMode: true-- enables strict React rendering checksoutput: "standalone"-- minimal production bundle
| Boundary | File | Behaviour |
|---|---|---|
| Route error | app/error.tsx |
Displays error.message + error.digest (opaque hash) |
| Global error | app/global-error.tsx |
Displays error.message + error.digest |
| Not found | app/not-found.tsx |
Generic 404 -- no sensitive information |
- API routes return generic error messages (e.g. "Failed to create run").
- Full error details are logged server-side via the structured logger.
- Stack traces are never included in HTTP responses.
- The health endpoint may expose database/warehouse error messages in the
checks.*.errorfield (connection-level errors only).
In production (NODE_ENV=production):
- Next.js automatically sanitises error messages in error boundaries, replacing
them with generic text and an opaque
digesthash. - Server-side logs use structured JSON format for log aggregation.
flowchart LR
subgraph logSources [Log Sources]
AgentLog[AI Agent<br>promptKey, model, duration, honestyScore]
PipelineLog[Pipeline Engine<br>step timing, runId]
StepLog[Pipeline Steps<br>domain, useCaseCount]
end
subgraph logOutput [Output]
DevFormat[Dev: Formatted text]
ProdFormat[Prod: JSON to stdout]
end
AgentLog --> ProdFormat
PipelineLog --> ProdFormat
StepLog --> ProdFormat
AgentLog --> DevFormat
PipelineLog --> DevFormat
StepLog --> DevFormat
| Field | Logged With |
|---|---|
runId |
All pipeline and engine logs |
promptKey |
Every LLM call |
promptVersion |
SHA-256 hash of the template used |
model |
Model endpoint name |
durationMs |
LLM call duration |
responseChars |
LLM response size |
honestyScore |
LLM self-assessment (0.0-1.0) |
temperature |
LLM temperature parameter |
Each pipeline run stores:
| Field | Purpose |
|---|---|
appVersion |
Application version at time of execution |
promptVersions |
SHA-256 hash of every prompt template used |
stepLog[] |
Per-step timing: startedAt, completedAt, durationMs, error |
aiModel |
Model endpoint used |
config |
Full run configuration (business name, metadata path, options) |
createdAt / completedAt |
Timestamps |
This provides full reproducibility: given a run's promptVersions and
appVersion, you can reconstruct exactly which prompt text and application
code produced the results.
All external API calls use AbortController-based timeouts to prevent
indefinite hangs:
| Operation | Timeout |
|---|---|
| OAuth token exchange | 15 seconds |
| SQL statement submission | 30 seconds |
| SQL statement polling | 15 seconds |
| SQL chunk fetching | 30 seconds |
| Workspace API calls | 30 seconds |
| Condition | Behaviour |
|---|---|
| 5xx errors | Retry with exponential backoff (2s, 4s, 8s, max 10s) |
| Timeout / network errors | Retry |
| 4xx errors | No retry (non-retryable) |
INSUFFICIENT_PERMISSIONS |
No retry |
SQL syntax errors (SQLSTATE: 42) |
No retry |
| Default max retries | 1 (configurable per call) |
Each pipeline step has independent error handling:
| Step | Failure Behaviour |
|---|---|
| Business context | Falls back to default context |
| Table filtering | Fail-open: includes all tables |
| Use case generation | Failed batches skipped; successful batches kept |
| Domain clustering | Falls back to "General" domain |
| Scoring | Default scores (0.5) on failure |
| Cross-domain dedup | Skipped on failure (no removals) |
| SQL generation | Per-use-case: marks as "failed"; attempts fix via USE_CASE_SQL_FIX_PROMPT |
# Stage 1: Build (builder) -- install deps, generate Prisma, build Next.js
FROM node:18-alpine AS builder
# Stage 2: Run (runner) -- minimal runtime with standalone output only
FROM node:18-alpine AS runner
ENV NODE_ENV=production| Control | Detail |
|---|---|
| Base image | node:18-alpine (minimal attack surface) |
| Multi-stage | Build dependencies not included in runtime image |
| No secrets in image | All credentials provided at runtime via env vars |
| Production mode | NODE_ENV=production -- disables dev tooling |
| Standalone output | Only the minimum files needed to run are copied |
| No root user | Runs as default Node.js user in Alpine |
- Dependencies pinned via
package-lock.json(npm cifor deterministic installs). - No post-install scripts beyond Prisma client generation.
flowchart TD
subgraph public [Public API Surface]
Health[GET /api/health]
Runs[POST /api/runs]
RunsList[GET /api/runs]
RunDetail[GET /api/runs/:runId]
RunDelete[DELETE /api/runs/:runId]
RunExec[POST /api/runs/:runId/execute]
Metadata[GET /api/metadata]
Export[GET /api/export/:runId]
end
subgraph auth [Auth Layer]
DatabricksProxy[Databricks Apps Proxy<br>SSO + token injection]
end
Browser[User Browser] -->|HTTPS| DatabricksProxy
DatabricksProxy --> public
| Threat | Mitigation | Residual Risk |
|---|---|---|
| Unauthenticated access | Databricks Apps proxy requires SSO login | None when deployed as Databricks App |
| SQL injection via catalog/schema | validateIdentifier() with strict regex |
Low |
| Prompt injection via user input | Delimiter wrapping + marker stripping | Medium -- LLMs are inherently susceptible |
| Cross-site scripting (XSS) | React auto-escaping + X-Content-Type-Options: nosniff |
Low |
| Clickjacking | X-Frame-Options: DENY |
None |
| Denial of service | Fetch timeouts + SQL Warehouse concurrency limits | Medium -- no app-level rate limiting |
| Data exfiltration via LLM | Only metadata sent; sample data opt-in; no PII by design | Low |
| Insecure deserialization | Zod validation on all API inputs | Low |
| Broken access control | UC permissions enforced by Databricks; per-user isolation enforced at app layer (owner_email + forge_resource_acl); middleware-enforced auth seam |
Low |
| Information leakage | Error boundaries show opaque digests in production; health endpoint may expose connection errors | Low |
| Supply chain | Pinned deps via lockfile; Alpine base image | Low |
| Category | Handling |
|---|---|
| User identity | Email read from proxy headers; used only for notebook export path (/Users/<email>/). Not stored in Lakebase. |
| Business data (row-level) | Not accessed in default mode. When sampling is enabled, values are truncated, used transiently in-memory, and never persisted or exported. |
| Metadata | Table/column names may be considered business-confidential but are not personal data. |
| LLM-generated content | Contains no personal data; derived from metadata and business configuration. |
| Principle | Implementation |
|---|---|
| Data minimisation | Only metadata is read by default; sample data is opt-in with configurable limits (0-50 rows) |
| Purpose limitation | Data is used exclusively for use case discovery and SQL generation |
| Storage limitation | Runs can be deleted via UI; cascade deletes remove all associated data |
| No external transfers | All processing occurs within the Databricks workspace; no data leaves the customer's environment |
| Right to erasure | DELETE /api/runs/:runId removes all associated data |
All data processing and storage occurs within the customer's Databricks workspace. The application does not transmit data to any external service, third-party API, or cross-region endpoint.
| Area | Resolution |
|---|---|
| CSP header | Content-Security-Policy configured in next.config.ts (default-src 'self', script-src 'self' 'unsafe-inline', frame-ancestors 'none', etc.) |
| HSTS header | Strict-Transport-Security: max-age=63072000; includeSubDomains; preload added to next.config.ts |
| App-level rate limiting | In-memory sliding-window limiter in lib/rate-limit.ts, activated via proxy.ts (LLM routes: 3k/min, general: 12k/min per client) |
| Health endpoint exposure | /api/health returns only { status, version, uptime, timestamp } for unauthenticated callers; full diagnostics (checks, authRuntime, host) require auth headers |
| Error message sanitisation | safeErrorMessage() in lib/error-utils.ts returns generic message in production; applied to all API route error responses. Error boundaries rely on Next.js production sanitisation |
| Prompt injection (Ask Forge) | User question now wrapped in ---BEGIN USER QUESTION--- / ---END USER QUESTION--- delimiters with marker stripping in lib/assistant/prompts.ts |
| Sample data audit logging | fetchSampleData() logs structured audit entry with table FQNs, runId, userEmail, and step |
| Dependency audits in CI | npm audit --audit-level=high step added to .github/workflows/ci.yml |
| Per-user isolation (full) | Every root table has owner_email (NOT NULL); list, detail, update, delete, and export paths apply the visibility rule (owner OR shared); team-shared opt-in via ForgeResourceAcl; vector search filters by accessible parent ids; auth seam enforced via proxy.ts + requireUser(); Server Components call requireUser() directly. |
| Per-user fairness | Active-resource caps per user (FORGE_MAX_ACTIVE_*_PER_USER); pipeline runs queue when over cap; pool rate limiter tracks per-user inflight per endpoint; system-load banner surfaces aggregate load anonymously. |
| Area | Limitation | Risk Level |
|---|---|---|
| Group sharing | Sharing is per-resource per-email only. Group/team sharing (ForgeTeam) is a documented follow-up. |
Low |
| Owner transfer / GDPR erasure | Bulk owner transfer and right-to-erasure require manual SQL today. The schema supports both (cascading deletes are wired) but a single lib/lakebase/erase-user.ts orchestrator and admin UI is a follow-up. |
Low |
| Schema rollback | The migration is forward-only (owner_email is NOT NULL). FORGE_USER_ISOLATION=false does not restore pre-isolation behaviour because the data layer always filters by owner_email. Real rollback requires a Lakebase point-in-time restore. |
Medium (deploy-time only) |
| Prompt injection | Delimiter-based mitigation reduces but cannot eliminate risk (inherent LLM limitation) | Medium |
unsafe-inline in CSP |
script-src 'self' 'unsafe-inline' still required by Next.js; nonce-based CSP would be stronger |
Low |
- Group / team sharing -- add
ForgeTeam+ForgeTeamMemberand an ACLviewerType: email | teamto grant access to a whole team in one row. - Owner transfer & GDPR erasure -- add a single
lib/lakebase/erase-user.tsorchestrator and admin UI to bulk-transfer or delete a user's resources (the schema already supports cascading deletes via theowner_emailcolumn). - SCIM email autocomplete -- replace the free-text recipient input on
the share dialog with SCIM directory autocomplete
(
/api/2.0/preview/scim/v2/Users). - Notifications on share -- in-app + email/Slack notification when a
resource is shared with you; today the activity log records
resource_shared/resource_unsharedfor future wiring. - Nonce-based CSP -- replace
'unsafe-inline'with per-request nonces once Next.js supports it natively. - Per-user LLM budget enforcement --
ForgeUsageis read-only in v1.0.0; turn it into a hard cap once usage data is available.
| Variable | Required | Source | Description |
|---|---|---|---|
DATABRICKS_HOST |
Yes | Platform | Workspace URL (e.g. https://workspace.cloud.databricks.com) |
DATABRICKS_WAREHOUSE_ID |
Yes | app.yaml | Bound SQL Warehouse ID |
LAKEBASE_ENDPOINT_NAME |
Auto | Startup script | Lakebase endpoint resource name used by /api/2.0/postgres/credentials |
LAKEBASE_POOLER_HOST |
Auto | Startup script | Pooler hostname for runtime queries |
LAKEBASE_USERNAME |
Auto | Startup script | Cached runtime username (service principal identity) |
DATABASE_URL |
Dev only | .env.local |
Local development Lakebase PostgreSQL connection string |
DATABRICKS_CLIENT_ID |
Auto | Platform | Service principal client ID |
DATABRICKS_CLIENT_SECRET |
Auto | Platform | Service principal client secret |
DATABRICKS_TOKEN |
Dev only | .env.local |
Personal access token for local development |
DATABRICKS_APP_PORT |
Optional | Platform | Listen port (default: 3000) |
LOG_LEVEL |
Optional | .env |
Minimum log level: debug, info, warn, error |
NODE_ENV |
Auto | Dockerfile | production in deployed image |
| Control | OWASP Top 10 | SOC 2 | Implementation |
|---|---|---|---|
| Authentication | A07:2021 | CC6.1 | Databricks SSO via Apps proxy |
| Input validation | A03:2021 | CC6.1 | Zod schemas + regex validation |
| Injection prevention | A03:2021 | CC6.1 | validateIdentifier() + parameterised patterns |
| Security headers | A05:2021 | CC6.6 | X-Frame-Options, X-Content-Type-Options, etc. |
| Logging | A09:2021 | CC7.2 | Structured JSON logging with run correlation |
| Error handling | A04:2021 | CC7.3 | Error boundaries with opaque digests |
| Secrets management | A02:2021 | CC6.1 | Environment-only; no hardcoded credentials |
| Data minimisation | -- | CC6.5 | Metadata-only by default; row access opt-in |