Internal, read-first SQL explorer for your organization's databases — with built-in PHI tokenization, an AI query assistant, a governed write-approval workflow, multi-database support, and per-environment capability-based access control.
One pre-configured place where every team member can query the databases they're allowed to reach, without juggling credentials or desktop clients — and without PHI ever leaving the building in the clear.
- Multi-database support — PostgreSQL, SQL Server (MSSQL), MongoDB, and Elasticsearch, each with a dialect-aware editor, autocomplete, query templates, and default result view.
- Monaco editor — syntax highlighting and schema-aware autocomplete (tables, columns, PK and 🔐 PHI markers; Mongo
db.collection.find()style; ES/_searchpaths). Language adapts to the connection type. - Multi-tab workspace — independent tabs, inline rename, per-tab schema selection, and run-selection-or-whole-query (Cmd/Ctrl+Enter).
- Schema browser — connections grouped by environment; lazy table/column tree with PK/type/PHI flags; double-click a table to open a
SELECT * … LIMIT 100tab; copy table metadata as JSON / DDL / text. - Results grid (AG Grid) — sort/filter/resize, type-aware cell coloring, table ↔ JSON view toggle, row count / exec time / truncation badges.
- Cell inspector drawer — click a long cell to open a docked read-only inspector (auto-detects JSON vs text); double-click to copy; hover for a peek.
- Local-timezone datetimes — ISO datetime cells show a your-time conversion alongside the original source value on hover.
- Query history — your last executed queries, one click to reload.
- Saved queries — save (shared by default), search, load into a new tab; used as few-shot examples by the AI assistant.
- Tab persistence — open tabs, active connection, and sidebar state survive a reload (results are not persisted).
- Export — download results as CSV or JSON (masking enforced and audited).
- PHI tokenization ("shield") — pattern-based column matching with four masking types: FULL (
********), PARTIAL (last 4 shown), HASH (tok_…deterministic), REDACT ([REDACTED]). - Per-environment enforcement — every production environment (any name containing "prod":
PROD,SUPER_PROD, …) is always tokenized and cannot be turned off; the rest of the masked list is configurable. QA/DEV return real values by default. alwaysMasked(locked) rules — never unmask regardless of role or shield state.- Audited unmasking — de-tokenizing requires a reason (and optional notes), is gated per-environment, and is logged with user, IP, session, and timestamp. Unauthorized attempts stay masked and are logged as denied.
- Write mode with a request → review → approval → execute lifecycle. Globally toggleable by admins.
- Two-person rule on production — writes to any production environment (any name containing "prod") always require a second approver and can never be direct-execute; other environments can be configured for direct write.
- Paired verify SELECT — every write request carries a read-only SELECT to preview affected rows before execution.
- AI safety review — verdict (Safe / Caution / Dangerous), blast-radius estimate, SELECT-matches-write check, and one-click suggested corrections.
- Single-statement, scoped writes only — one INSERT/UPDATE/DELETE (or Mongo/ES equivalent); UPDATE/DELETE must be scoped (WHERE required); transactional where the engine supports it.
- Save without running — "Save request" stores a request as a draft that executes nothing until someone submits or runs it, on any environment. This is also what the MCP endpoint creates, so an AI agent can propose a change but never make one.
- Full lifecycle audit — save, submit, AI review, approve/reject, execute/fail, cancel, revise & resubmit — each with an activity timeline.
- Natural-language → query for read and write modes, dialect-aware, with a table-selection pass for large schemas and few-shot examples pulled from saved queries.
- Schema-only — only schema metadata is sent to Azure OpenAI; never row data. Every generation is logged (prompt, response, model, tokens, latency) for admin review.
- Hosted MCP endpoint at
/api/mcp— agents discover connections, browse schema, and run read-only queries through the same API the UI uses, so capabilities, PHI tokenization, row caps and audit logging all apply unchanged. Configure with just a URL, username, and password; nothing to install. - Agents propose, humans dispose — an agent can save a write request as a draft (statement plus its verify SELECT); it cannot submit, approve or execute one, so nothing an agent writes reaches a database until a person acts on it in D-Pilot.
- Shareable documents that live next to the data — prose plus runnable read queries, opened as a tab from
/artifacts/:idby anyone who can log in. Each query block has its own Run button and its own results. - Stores queries, never rows — running a block goes through the normal read path, so every reader gets their own capability checks, their own PHI tokenization and their own audit entry. An artifact cannot carry a snapshot past masking policy.
- Markdown prose, safely rendered — text blocks are GitHub-flavoured markdown (headings, bold, lists, tables). The renderer never emits raw HTML, so a document written by one colleague cannot script the app for everyone who opens the link.
- Archive, never delete — a shared link is somebody else's bookmark, so artifacts can only be archived, and archiving is reversible. Archived artifacts drop out of listings but their links still open.
- Agent-authored — the MCP endpoint can create, update and delete them, so an AI agent's analysis lands somewhere the whole organization can open instead of in one person's chat window.
- Capability-based access control — each user has an
isAdminflag plus four per-environment capability lists: read, unmask PHI, write, and approve. Admin implies all capabilities on all environments. - Audit log — every query, error, export, PHI unmask (and denial), and write-lifecycle event is recorded, with date/type filtering and automatic 30-day archival to a separate database.
- Usage analytics — admin dashboard: users, DAU/WAU/MAU, query volume/latency, AI usage and success rate, PHI unmasks, top users, per-connection activity.
- Read-only enforcement — the read path blocks all DML/DDL; writes only ever go through the governed write workflow (a separate executor).
- White-label branding — custom app name, logo (light/dark), and favicon via environment variables.
- Installable app (PWA) — install to the desktop or home screen for a standalone window with an identical UI; the manifest picks up
APP_NAME, and new deploys prompt to reload. No API response is ever cached, so PHI never reaches on-disk storage. See Install as an App.
| Layer | Technology |
|---|---|
| Frontend | React 19, TypeScript, Vite, Mantine UI v8, Monaco Editor, AG Grid, Zustand |
| Backend | Node.js, Express, TypeScript |
| App Database | SQLite (via better-sqlite3, WAL mode) |
| Auth | JWT (bcrypt + jsonwebtoken) |
| AI | Azure OpenAI (optional) |
# Install dependencies
npm install
# Copy and configure environment
cp .env.example .env
# Edit .env with your settings (see Configuration below)
# Development (hot-reload client + server)
npm run dev
# Production build
npm run build
npm startBy default, the dev client runs at http://localhost:3100 and the server at http://localhost:3101.
Both ports are configured in .env:
| Variable | Purpose | Default |
|---|---|---|
PORT |
Server port (also used as proxy target in dev) | 3101 |
VITE_PORT |
Dev client port | 3100 |
By default D-Pilot assumes it owns the root of whatever domain it is served on
(https://d-pilot.internal/). If a reverse proxy instead mounts it under a
prefix of a shared domain — https://intranet.example/d-pilot — set BASE_PATH.
Read "The prefix is not a security boundary" below before choosing that domain:
sharing one with another app has consequences a path prefix does not contain.
BASE_PATH=/d-pilotThen build and start with that variable set:
BASE_PATH=/d-pilot npm run build # bakes the prefix into the asset URLs
BASE_PATH=/d-pilot npm start # mounts Express at the same prefixBoth steps matter, and they have to agree. Vite writes the prefix into every
<script>, stylesheet and icon URL at build time; Express reads it at startup
and mounts its API, its manifest and the built client there. Putting it in .env
is the reliable way to keep them in step — deploy.sh builds and restarts in one
go, and both halves read the same file. Changing BASE_PATH requires a
rebuild, not just a restart; a restart alone leaves a client full of URLs
pointing at the old prefix.
Because the server mounts itself under the prefix, the proxy is a plain pass- through with no path rewriting:
location ^~ /d-pilot/ {
proxy_pass http://10.0.0.5:3101; # no trailing slash: pass the path through as-is
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}Two things are easy to get wrong here. A trailing slash on proxy_pass
strips the prefix before it reaches D-Pilot, which now expects to see it — leave
it off. And if the same server block also serves another app at the root, ^~
is required rather than a plain prefix: a regex location for static file
extensions takes priority over a plain prefix location, so without it every
/d-pilot/assets/*.js request is answered by the root app instead.
In local development, npm run dev reads .env for both halves and needs
nothing extra. npm run dev:local does not: the server half is pinned to
.env.dev while the Vite half reads .env and .env.dev, so a BASE_PATH
set only in .env serves the client under the prefix and proxies to a server
still mounted at the root — every API call 404s. Set it in both files.
What this does not cover is a deployment that cannot set BASE_PATH at build
time — a prebuilt artifact, say. There is no runtime switch for it, because the
prefix is inside the emitted bundle filenames and the service worker's scope.
BASE_PATH puts D-Pilot on its own path, but browser storage is scoped per
origin. D-Pilot keeps the session JWT, the signed-in user and the persisted
tab workspace — which holds the SQL you have been writing — in localStorage,
so every one of those is readable by any script running anywhere else on the
same domain, and D-Pilot can read theirs. A cross-site scripting flaw in an
unrelated app at / is therefore a full session takeover of a tool that reaches
PHI, with no flaw in D-Pilot at all.
Mount D-Pilot under a prefix of a domain whose other occupants you trust as much as D-Pilot itself — ideally a dedicated internal hostname, which is what the default root deployment gives you for free. This is the same reasoning that keeps API responses out of the service worker's Cache Storage: the protection D-Pilot applies to a query result is worth nothing once the data is somewhere the browser hands to anyone sharing the origin.
Copy .env.example to .env and set the following:
# Server
PORT=3101
VITE_PORT=3100
NODE_ENV=production
# JWT — CHANGE THIS to a random string (e.g. openssl rand -hex 32)
JWT_SECRET=<your-random-secret-here>
JWT_EXPIRES_IN=24h
# Branding
APP_NAME="Your App Name"
LOGO_URL=/logo/your-logo.svg # dark-ink mark, shown on the light theme (empty = text-only)
LIGHT_LOGO_URL=/logo/your-logo-light.svg # light-ink mark, shown on the dark theme and the login panel
FAVICON_URL=/logo/favicon.svg # optional
# Email domain — enforced on user creation, used for the default admin seed
EMAIL_DOMAIN=yourcompany.com
# Default admin password (only used on first run to seed the admin user)
DEFAULT_ADMIN_PASSWORD=<strong-password>
# Database connections — JSON array
DBFORGE_CONNECTIONS='[
{
"id": "qa-pg",
"name": "QA PostgreSQL",
"env": "QA",
"type": "postgres",
"host": "your-db-host",
"port": 5432,
"database": "your_database",
"username": "your_user",
"password": "your_password",
"schema": "public"
}
]'
# Query safety
MAX_ROWS=10000 # hard cap on returned rows
QUERY_TIMEOUT_MS=90000 # per-query timeout
# Results "Copy as" formats (optional; JSON array). Unset = built-in defaults
# (CSV, TSV, JSON, Markdown, SQL INSERT, comma, single-quoted, double-quoted).
# COPY_FORMATS='[{"id":"in-clause","label":"SQL IN","group":"List / SQL","columnMenu":true,"template":{"columnSeparator":",","rowSeparator":",","quote":"single","prefix":"IN (","suffix":")"}}]'
# MCP endpoint for AI agents — rows returned per query (agents may raise per call)
MCP_MAX_ROWS=1000
# Schema cache (autocomplete + AI); hours, 0 disables
SCHEMA_CACHE_TTL_HOURS=24
# AI assistant — Azure OpenAI (optional; endpoints return 503 if unset)
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
AZURE_OPENAI_KEY=<your-key>
AZURE_OPENAI_DEPLOYMENT=<your-deployment-name>
AZURE_OPENAI_MODEL=gpt-4o # optional, informational
# AZURE_OPENAI_API_VERSION=2024-08-01-preview
# AZURE_OPENAI_INSECURE_TLS=true # only for TLS-inspecting corporate proxiesNote: PHI masking and the write workflow are configured in-app (Settings), not via environment variables — masked environments, PHI rules, write-mode toggle, and direct-write environments are stored in the SQLite app database. (The legacy
PHI_ALWAYS_MASKED/PHI_ADMIN_CAN_UNMASKenv vars are no longer read.)
| Type | Required fields |
|---|---|
postgres |
host, port, database, username, password, schema (optional) |
mssql |
host, port, database, username, password |
mongodb |
uri (full connection string) |
elasticsearch |
host, port, username, password, and schema set to http or https (used as the protocol) |
Access is capability-based and scoped per environment (DEV, QA, UAT, STG, PROD
by default — see Custom environments).
Each user has an isAdmin flag plus four capability lists, managed from Settings → User Management:
| Capability | Grants |
|---|---|
Read (allowedEnvironments) |
Query connections in those environments |
Unmask PHI (unmaskEnvironments) |
De-tokenize PHI (still audited) in those environments |
Write (writeEnvironments) |
Author write requests for those environments |
Approve (approveEnvironments) |
Approve others' write requests in those environments |
Admin implies every capability on every environment. Production safety rails (mandatory PHI tokenization and two-person write approval) always apply regardless of capabilities, on every environment whose name contains "prod" — see Custom environments.
The environment list is not hardcoded — it is derived from the env values in
DBFORGE_CONNECTIONS. Adding a connection with a new env is all it takes:
After a restart, SUPER_PROD shows up on its own in the connection tree, the four
capability pickers in Settings → User Management, the PHI masked-environment toggles
and the write policy. Only environments the deployment actually has are accepted by the
API, so a stale name can't be granted to anyone.
Notes:
- Custom names sort after
PRODand are treated as the most sensitive. - Any environment whose name contains "prod" gets the full production rails —
PROD,SUPER_PRODandPREPRODalike are always PHI-tokenized and can never be direct-write (every change needs a second approver). Both toggles for them in Settings are locked. Naming a production environment something new does not opt it out. - Existing users don't gain a new environment automatically; grant it per user.
D-Pilot hosts a read-only (see the write-request exception below) MCP endpoint at
/api/mcp, so AI agents can query the same databases under the same rules as the UI.
It runs inside the existing server process — nothing extra to deploy, install, or clone.
Agents just point at the URL everyone already uses.
Its tools call D-Pilot's own REST API over loopback, so environment capabilities, PHI tokenization, row limits and audit logging are enforced exactly as they are for the UI and cannot be bypassed. Every agent query lands in the audit log under its service account.
| Tool | Does |
|---|---|
whoami |
Which account is connected and which environments it may read |
list_connections |
Databases available, per environment (optional env filter) |
list_schemas |
Schemas on a connection (Postgres/SQL Server) |
list_tables |
Tables, collections or indices |
describe_table |
Columns, types, nullability |
run_query |
Runs a read-only query, returns rows |
create_artifact |
Publishes a shareable document (prose + runnable read queries), returns its link |
update_artifact |
Edits an artifact the service account created |
get_artifact |
Reads one artifact's full body |
list_artifacts |
Artifacts visible to the account (optional search) |
archive_artifact |
Archives (or restores) an artifact the service account created |
create_write_request |
Saves a draft change request for a human to review — it never runs |
get_write_request |
Reads one request back, including the AI safety review a human ran on it |
update_write_request |
Edits a draft the service account saved — drafts only, and it stays a draft |
Database writes are deliberately not exposed — those stay in the write-approval workflow, where a human reads the paired verify SELECT and a second person approves.
create_write_request is how an agent proposes a change without being able to make one. It
saves a DRAFT: title, the write statement, and the verify SELECT that previews the affected
rows, validated by exactly the same rules the UI applies. A draft executes nothing — not even
on an environment configured for direct writes — until a person opens it in D-Pilot and clicks
Submit for approval or Run now. There is no tool to submit, approve, run or delete one,
and the account still needs Write capability on the target environment to save a draft at all.
Submitting also makes the submitter the requester, so nobody can have an agent draft a change,
submit it, and then approve their own work.
get_write_request closes the review loop: after someone clicks Review with AI on the
request, the agent can read the verdict, the risks and the suggested statements back, and fix
the draft with update_write_request. Editing is drafts only — the moment a person submits,
approves or runs a request, the agent can no longer change what they are signing off on — and an
edited draft stays a draft, so revising one can never become a way to run it.
The artifact tools are the one exception to read-only, and only because they touch no target
database: an artifact holds prose and unexecuted read queries in D-Pilot's own SQLite, an
agent may only edit the ones its own account created, and nothing it writes reaches a database
until a human opens the artifact and runs a block as themselves. Set APP_BASE_URL so the
tools hand back clickable links instead of bare /artifacts/<id> and /write-requests/<id>
paths.
-
Pick the account. Either create a service account in Settings → User Management, or let each person connect with their own login — then every agent action is audited under that person and bounded by their capabilities. Grant only the Read capability, on only the environments agents should reach — not an admin. Add Write on an environment only if agents should be able to save draft write requests there (they still cannot run one).
-
Point the agent at the URL with its username and password. For Claude Code:
claude mcp add --transport http d-pilot https://d-pilot.internal/api/mcp \ --header "Authorization: Basic $(printf '[email protected]:the-password' | base64)"Or in any MCP client's config file:
{ "mcpServers": { "d-pilot": { "type": "http", "url": "https://d-pilot.internal/api/mcp", "headers": { "Authorization": "Basic YWdlbnRAZXhhbXBsZS5jb206dGhlLXBhc3N3b3Jk" } } } }That
Basicvalue is justbase64("username:password")— generate it withprintf 'user:pass' | base64.
Why Basic auth: MCP clients can send static headers but cannot run D-Pilot's
username/password login flow, so /api/mcp accepts the credentials directly and exchanges
them for a normal JWT internally (kept in memory, never on disk, refreshed automatically).
Because the credentials cross the network on every request, serve D-Pilot over HTTPS if
agents connect from other machines — see the reverse-proxy section below.
Optional server-side setting:
| Variable | Default | Purpose |
|---|---|---|
MCP_MAX_ROWS |
1000 |
Rows returned per query. Agents are told this default (via whoami and the run_query schema) and can raise it per call with limit; MAX_ROWS remains the hard ceiling. |
APP_BASE_URL |
(unset) | The origin users browse D-Pilot on, used to build clickable artifact and write-request links in tool output. Just the origin — BASE_PATH is appended for you, and an origin that already spells the prefix out is left as written. Unset means agents return a bare /artifacts/<id> path, sub-path included. |
PHI: the endpoint never sends the unmask headers, so tokenized columns stay tokenized for
agents regardless of the account's capabilities. run_query names the tokenized columns so an
agent doesn't mistake mask characters for real values. Unmasking stays a UI-only action that
requires a human-supplied reason.
Revoking access: delete (or change the password of) the service account. Note this stops
new logins but does not kill a token already issued — as everywhere else in D-Pilot, a JWT
stays valid until it expires, so revocation takes effect within JWT_EXPIRES_IN (24h by
default). Lower that value if you need a tighter window.
D-Pilot is a Progressive Web App, so it can be installed as a standalone desktop or home-screen app. The installed app is the web app, unchanged — same layout, same routes, same behavior — just in its own window without browser chrome.
Installing. In Chrome or Edge, use the install icon in the address bar, or the Install button in the footer (it appears only while the browser reports the app as installable, and disappears once installed). On iOS, use Safari's Share → Add to Home Screen.
A service worker requires a secure context: HTTPS, or localhost. Behind the Nginx
setup below with a certificate, this works out of the box; over plain http://<lan-ip> the
app still runs normally but cannot be installed.
Branding. The manifest is rendered by Express at /manifest.webmanifest from
APP_NAME, so the installed app carries your deployment's own name. The icons are the
bundled PNGs in public/ (pwa-192.png, pwa-512.png, pwa-maskable-512.png,
apple-touch-icon.png) — installers require raster icons at declared sizes, so unlike
LOGO_URL they are not taken from a URL. To use your own mark, either replace those files
or edit scripts/generate-pwa-icons.mjs and run:
npm run icons:pwaUpdates. After a deploy, open clients show a New version available notification with a Reload button. Nothing reloads on its own — an unattended reload would discard open editor tabs and any in-flight query.
What is cached — and what is deliberately not. Only the app shell is cached (HTML, JS, CSS, icons, fonts; ~2.5 MB), plus the Monaco editor and its language workers fetched on first use (~10 MB, kept out of the install so it stays lean).
No API response is ever cached. Query results, schema listings, saved queries, exports and audit records all carry PHI, and Cache Storage is unencrypted on disk and survives a logout. Every
/apicall goes to the network every time, which is enforced by a test (vite.pwa.config.test.ts). The practical consequence: the installed app is not usable offline — it will load and show its shell, but running a query needs the server. Offline write queueing is likewise absent by design, since replaying writes later would reorder the write workflow's audit trail.
The service worker is disabled in npm run dev (it would serve stale modules and break
HMR). Verify PWA behavior against a production build — npm run build && npm start.
- SQLite database created at
data/dbpilot.sqlite(WAL mode) - Default admin user seeded:
admin@<EMAIL_DOMAIN>withDEFAULT_ADMIN_PASSWORD, granted all capabilities on all environments - Default PHI masking rules seeded (name / DOB / phone / email / address / ZIP patterns, PARTIAL masking)
- Default app settings seeded: PHI masked on
PROD, write mode enabled, direct-write onDEV
git clone <repo-url> /opt/d-pilot
cd /opt/d-pilot
npm installnpm run buildThis produces:
dist/client/— optimized frontend (HTML, JS, CSS)dist/server/— compiled backend
npm startThe app will be available at http://<server-ip>:<PORT> (default 3101, configurable via .env).
If you set LOGO_URL=/logo/your-logo.svg, place the file at:
mkdir -p public/logo
cp /path/to/your-logo.svg public/logo/your-logo.svgFor custom fonts (e.g. Barlow):
mkdir -p public/fonts/Barlow
cp /path/to/Barlow-*.ttf public/fonts/Barlow/These directories are gitignored — they won't be pushed back to the repo.
Create /etc/systemd/system/d-pilot.service:
[Unit]
Description=D-Pilot Internal Query Tool
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/d-pilot
ExecStart=/usr/bin/node dist/server/index.js
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.targetEnsure the www-data user owns the application directory:
sudo chown -R www-data:www-data /opt/d-pilotEnable and start:
sudo systemctl daemon-reload
sudo systemctl enable d-pilot
sudo systemctl start d-pilot
# Check status
sudo systemctl status d-pilot
# View logs
sudo journalctl -u d-pilot -fTo serve on port 80/443 or add SSL, put Nginx in front.
Install: sudo apt install nginx (Ubuntu/Debian)
Create /etc/nginx/sites-available/d-pilot:
server {
listen 80;
server_name _; # or your domain/IP
location / {
proxy_pass http://127.0.0.1:3101;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_cache_bypass $http_upgrade;
}
}sudo ln -s /etc/nginx/sites-available/d-pilot /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginxNow accessible at http://<server-ip> (port 80).
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d your-domain.com# If using Nginx (port 80/443)
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# If accessing Node directly (port 3101)
sudo ufw allow 3101/tcpcd /opt/d-pilot
git pull
npm install
npm run build
# Restart the service
sudo systemctl restart d-pilot # systemd
# or
pm2 restart d-pilot # pm2The SQLite database (
data/dbpilot.sqlite) persists across updates. Users, saved queries, PHI rules, audit logs, write requests, and settings are preserved.
The stateful files live in data/ — back them up regularly:
cp data/dbpilot.sqlite data/dbpilot-backup-$(date +%Y%m%d).sqlite
# audit_archive.sqlite holds audit entries older than 30 days
cp data/audit_archive.sqlite data/audit_archive-backup-$(date +%Y%m%d).sqlite| Issue | Fix |
|---|---|
EADDRINUSE on startup |
Another process is using the port. lsof -i :3101 to find it |
| Database connection errors | Verify the server can reach DB hosts: telnet <host> <port> |
| AI assistant returns 503 | Set the AZURE_OPENAI_* env vars; use Settings → Azure OpenAI → Test Connect |
| Logo not showing | Check file exists at public/logo/ and LOGO_URL matches the path |
| Forgot admin password | Delete data/dbpilot.sqlite and restart — re-seeds from .env |
Permission denied on data/ |
chown -R www-data:www-data /opt/d-pilot/data |
{ "id": "sp-pg", "name": "Super Prod PostgreSQL", "env": "SUPER_PROD", "type": "postgres", … }