Browse your database and run read-only SQL queries directly inside the Strapi admin panel — no separate DB client needed.
released July 14, 2026
npm install @swamp-crocodile/strapi-plugin-dbviewA read-only database browser and SQL query runner for the Strapi v5 admin panel.
Inspired by filament-dbview — the same tool, built for Strapi.
SELECT queries with a CodeMirror SQL editor. Supports EXPLAIN and EXPLAIN ANALYZE. Save queries for later.INSERT, UPDATE, DELETE, or DDL can ever execute, regardless of how the SQL is crafted.password, *_token, *_secret cannot be named in Query Runner SQL (including under aliases, expressions, or WHERE/JOIN clauses). SELECT * still works; matching values are replaced with [REDACTED] in the result set.| Requirement | Version |
|---|---|
| Strapi | ^5.0.0 |
| Node.js | >=20.0.0 <=26.x.x |
| npm | >=6.0.0 |
Strapi v4 is not supported. This plugin targets Strapi v5 and above only.
npm install @swamp-crocodile/strapi-plugin-dbviewThe plugin reuses the host admin's @strapi/design-system and @strapi/icons rather than bundling its own copy — sharing them is what keeps React context intact. They are declared as peer dependencies, so if your app does not already have them as direct dependencies, install them alongside:
npm install @strapi/design-system @strapi/iconspnpm installs peers automatically by default; npm and yarn do not, and will fail the admin build with Could not resolve "@strapi/icons".
Register the plugin in your Strapi app's config/plugins.ts (or .js):
// config/plugins.ts
export default ({ env }) => ({
'strapi-dbview': {
enabled: true,
config: {
// All options are optional — defaults shown below
defaultRowLimit: 100,
maxRowLimit: 5000,
queryTimeoutSeconds: 15,
denyList: [],
redactedColumnPatterns: [
'password',
'*_token',
'*_secret',
'hash',
'salt',
'secret',
'private_key',
'reset_password_token',
'confirm_token',
],
// Optional Layer 5 — dedicated SELECT-only DB user (you create the role + grants).
// When set, ALL plugin reads (browser, query runner, schema) use this pool.
// When omitted, the plugin uses Strapi's default database connection.
//
// URL form (Postgres / MySQL):
// readOnlyConnection: env('DBVIEW_DATABASE_URL'),
//
// Or full Knex config:
// readOnlyConnection: {
// client: 'postgres', // or 'pg', 'mysql2', 'better-sqlite3', …
// connection: {
// host: env('DBVIEW_HOST', env('DATABASE_HOST', 'localhost')),
// port: env.int('DBVIEW_PORT', env.int('DATABASE_PORT', 5432)),
// database: env('DBVIEW_NAME', env('DATABASE_NAME')),
// user: env('DBVIEW_USER', 'dbview_ro'),
// password: env('DBVIEW_PASSWORD'),
// ssl: env.bool('DATABASE_SSL', false) && { rejectUnauthorized: false },
// },
// pool: { min: 0, max: 5 },
// },
},
},
});| Option | Type | Default | Description |
|---|---|---|---|
defaultRowLimit | number | 100 | Default number of rows returned per query/browse request |
maxRowLimit | number | 5000 | Hard cap on rows. Requests above this are clamped, not rejected |
queryTimeoutSeconds | number | 15 | Statement-level timeout applied inside the query transaction |
denyList | string[] | [] | Additional table names to block. Merged with the built-in deny list |
redactedColumnPatterns | string[] | see above | Glob patterns for sensitive columns. Query Runner rejects SQL that references matching column names; result sets still mask matching output keys as [REDACTED] |
readOnlyConnection | string | Knex.Config | — | Optional dedicated connection for all plugin DB reads (Layer 5). See below. |
readOnlyConnection)This is opt-in defence-in-depth. The plugin cannot create a database user for you — your ops/DBA must create a role with only the privileges you want (typically SELECT), then point the plugin at it.
Supported forms:
| Form | Example |
|---|---|
| Postgres URL | postgres://dbview_ro:secret@localhost:5432/myapp |
| MySQL URL | mysql://dbview_ro:secret@localhost:3306/myapp |
| Knex config object | { client: 'pg', connection: { host, user, password, database }, pool? } |
Behaviour:
strapi.db.connection (same as your app).SELECT 1 as a health check, and uses that pool for schema listing, Database Browser, FK preview, Query Runner, and EXPLAIN.readOnlyConnection to fall back to the default connection. The plugin never silently ignores a broken RO config and falls back to the full-privilege app user.connection fields are labels only; they do not switch databases.Example: PostgreSQL SELECT-only role
-- Run as a superuser / owner against your app database
CREATE ROLE dbview_ro LOGIN PASSWORD 'choose-a-strong-password';
GRANT CONNECT ON DATABASE your_database TO dbview_ro;
GRANT USAGE ON SCHEMA public TO dbview_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO dbview_ro;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO dbview_ro;
-- Optional: revoke tables you never want visible even via SELECT *
-- REVOKE SELECT ON TABLE admin_users FROM dbview_ro;// config/plugins.ts
readOnlyConnection: env('DBVIEW_DATABASE_URL'),
// e.g. DBVIEW_DATABASE_URL=postgres://dbview_ro:...@db-host:5432/your_databaseProduction recommendation: use
readOnlyConnectionwith a true SELECT-only role. Application-layer checks (lexer, AST, deny list, redaction, rollback) still apply on top.
After enabling the plugin, go to Settings → Roles in the Strapi admin and assign permissions to the roles that need access.
| Permission | What it unlocks |
|---|---|
plugin::strapi-dbview.browse | Database Browser page + schema/structure endpoints |
plugin::strapi-dbview.query | Query Runner — execute SELECT, EXPLAIN, EXPLAIN ANALYZE |
plugin::strapi-dbview.saved-queries.manage | Save, load, and delete saved queries |
Roles without browse or query permission see a "no permissions" screen. The DB View menu link itself is hidden from the sidebar for users who lack browse.
After installation and configuration:
SELECT query in the editor. Press Cmd+Enter (macOS) or Ctrl+Enter (Windows/Linux) to run it.maxRowLimit).SELECT * FROM … when the editor is empty).Queries are not persisted to a history table. Executions are written to the application log instead — blocked queries at
warnlevel, successful reads atdebug.
All SQL execution passes through independent security layers. Every layer must pass before a query touches the database. Bypassing one layer does not bypass the others.
A character-by-character state machine strips all string literals, quoted identifiers, comments (including MySQL /*!...*/ executable comments), and dollar-quoted strings before scanning. The stripped text is then checked for:
SELECT or WITHINSERT, UPDATE, DELETE, TRUNCATE, DROP, CREATE, ALTER, GRANT, REVOKE, EXEC, CALL, PRAGMA, VACUUM, SET, COMMIT, ROLLBACK, and moreSLEEP, BENCHMARK, PG_SLEEP, PG_READ_FILE, LOAD_FILE, XP_CMDSHELL, UTL_HTTP, DBMS_SQL, and more;)node-sql-parser parses the SQL and verifies:
SELECT statementWITH cte AS (...) SELECT * FROM cte works correctly), including every branch of a UNION and the real tables behind a CTE aliasWHERE, JOIN, CTEs, subqueries), including quoted identifiers. Aliases are not treated as source columns — SELECT password AS pwd still reports password.This layer fails closed: if the parser cannot positively confirm the statement is a SELECT — whether because it is a detected non-SELECT or because no supported dialect could parse it — the query is rejected. Allowing an unparseable query through would skip the table-scope check below (which runs on the parsed table list), so a query the parser cannot understand could otherwise reach a deny-listed table.
Every table referenced in the query must:
Built-in deny list (cannot be overridden by config):
strapi_core_store_settings strapi_database_schema strapi_migrations
strapi_migrations_internal admin_users admin_passwords
strapi_api_tokens strapi_api_token_permissions
strapi_transfer_tokens strapi_transfer_token_permissions
strapi_webhooks strapi_history_versions
strapi_releases strapi_release_actionsResult-column redaction alone is not enough: renaming a secret in the SELECT list (for example SELECT password AS pwd or SELECT password || '' AS x) would otherwise return the cleartext under a non-matching key.
After the AST is verified, every source column identifier from the parse is checked against redactedColumnPatterns. If any match, the query is rejected before execution — including when the name appears only inside a CTE, subquery, function call, or WHERE/JOIN clause.
SELECT * (and table.*) does not expand to concrete column names in the AST list, so those queries are allowed; sensitive values are still replaced with [REDACTED] in the result set (see below).
Result redaction (always applied after a successful execute): any result key whose name matches a redaction pattern is replaced with [REDACTED]. This covers SELECT * and any residual name-based leakage.
Prefer the Database Browser when you need a filtered view of tables that contain sensitive columns — it never accepts raw column expressions and redacts on the real schema names.
Every query is wrapped before execution:
SELECT * FROM (
<your query>
) AS _dbview_sub LIMIT <N>N is an integer constant derived from config, never from user input. The closing paren and LIMIT sit on their own lines so a trailing single-line comment (-- …) inside the inner query cannot comment out the enforced limit.
All queries — including EXPLAIN ANALYZE which actually executes the plan — run inside a transaction that unconditionally rolls back via a private sentinel exception:
await knex.transaction(async (trx) => {
rows = await trx.raw(wrappedSql);
throw new RollbackSignal(); // always rolls back
});Nothing a query does can ever persist, even if all other layers were somehow bypassed.
When readOnlyConnection is configured, every plugin read uses a dedicated Knex pool aimed at a DB user you provision with only SELECT (or tighter) grants. This is enforced at the database engine, independent of application logic.
SELECT 1).See Optional read-only connection for config shapes and grant examples.
| Database Browser | Query Runner | |
|---|---|---|
| Access | Point-and-click | Raw SQL |
| SQL injection risk | None (Knex query builder, no raw SQL) | Mitigated by multi-layer security model |
| Sensitive columns | Redacted in results; cannot filter/sort on them | Cannot be named in SQL; SELECT * redacts in results |
| Supported operations | Paginated read with sort/filter | SELECT, EXPLAIN, EXPLAIN ANALYZE |
| Export | CSV, JSON | CSV, JSON |
| Requires permission | browse | query |
# Clone the repo
git clone https://github.com/sridhar-s-subramanian/strapi-plugin-dbview.git
cd strapi-plugin-dbview
# Install dependencies
npm install
# Type-check
npm run test:ts:back
npm run test:ts:front
# Run the test suite (Vitest)
npm test
# Build
npm run build
# Verify plugin structure
npm run verify
# Watch mode (for development with a linked Strapi app)
npm run watch:link# In this repo
npm run watch:link
# In your Strapi app
npx yalc add @swamp-crocodile/strapi-plugin-dbviewnpm test # run once
npm run test:watchThe suite (Vitest) runs entirely on in-memory SQLite with no external database required:
UNION table extraction, column extraction (aliases/expressions), fail-closed behaviour.SELECT / WITH…SELECT statements against allowed tables can run — writes, DDL, CTE-smuggled writes, file/OS functions, deny-list evasion, parser-defeating reads, and sensitive-column alias/expression exfiltration are all rejected.useDbViewApi request contract (endpoints, encoding, unwrapped request bodies) and request-error message extraction.Tests live outside the compiled source tree (server/tests, admin/tests) so they never ship in the published package.
MIT
Share your work with the community and get it listed in the Strapi ecosystem for everyone to discover and use.
Submit
