Skip to content
Document DB v12 - Improved Interceptors with Soft Delete Integration, AI protections, & Admin UI with Aspire Integration! How!?

Admin UI (ShinyDocDbMyAdmin)

ShinyDocDbMyAdmin is a phpMyAdmin-style web front end for DocumentDb stores. Connect to a database, browse the documents in it, edit them, run SQL, manage JSON indexes, and move data in and out. No user management, no server administration — just the documents.

It is a Blazor Server app shipped as a container image and nothing else.

Terminal window
docker run -p 8085:8080 -v shiny-docdb-myadmin:/data ghcr.io/shinyorg/shiny-docdb-myadmin

Every relational DocumentDb backend:

ProviderConnectionNotes
SQLitefile, uploadable
SQLCipherfile, uploadableneeds the encryption key
DuckDBfile, uploadabletakes an exclusive lock while open
PostgreSQLserver
SQL Serverserver
MySQLserver
MariaDBserver
Oracle 23ai+server
CockroachDBserver

The document stores (MongoDB, Cosmos DB, LiteDB, IndexedDB, …) are deliberately out of scope: this tool works against the shared Id / TypeName / Data / CreatedAt / UpdatedAt envelope over ADO.NET, which only the relational providers expose.

Connection → table → TypeName, with live document counts. Tables are classified by probing for the envelope, so temporal/blob/spatial sidecars are recognised and kept out of the way rather than shown as if they were yours to edit.

Database overview listing the documents table alongside its temporal and spatial sidecars

A paged, sortable grid whose columns are inferred by sampling documents — nested paths like customer.tier become first-class columns. Filter on any envelope column or JSON path (=, , contains, starts/ends with, comparisons, null checks), or quick-search across the string fields.

Numeric filters and sorts compare numerically, so 9 < 10 rather than "9" > "10".

Browse grid for the Order type with columns inferred from nested JSON paths

Any row expands into a read-only, syntax-highlighted view of its whole body, with objects and arrays collapsible (native <details> elements, so it works with scripting off) and a toggle to the raw pretty-printed text.

A browse row expanded into a formatted, collapsible JSON view

Every value is HTML-escaped on the way out: document content belongs to whoever wrote it, and this markup is rendered raw.

A JSON editor with format and validation. Insert, edit, duplicate-by-id, single and bulk delete. Deleting a document also clears its blob sidecar rows.

The edit document dialog showing the id field and a formatted JSON body

The inferred shape of a type — paths, types, how often each field is actually present, and examples — plus row and size statistics and one-click create/drop of JSON property indexes, named exactly as DocumentDb names its own.

Structure tab showing inferred paths, types, presence percentages and per-path index buttons

Because the store is schema-free, this describes what the sampled documents actually contain — it is not a contract. A field below 100% is simply absent from some documents.

Appears when the table has a {table}_history sidecar, written by MapTemporal<T>. It opens on an audit log across the type — what changed, when, and by which actor.

History tab showing an audit log of versions across the Order type

Narrow to one document and you get its versions, each with its operation, the interval it was current, and how long it stood. Pick any two and compare them: a field-by-field list of what was added, removed or changed by dotted path (customer.tier, items[2].price), or the two bodies side by side. A prior version can be restored behind a two-click confirm, which writes it back as the current document.

Version timeline for one document with a field-level diff between version 1 and version 4

Appears when a type stores GeoJSON. Draws the geometries on a server-rendered SVG map with zoom-to-feature and fit-all, and lists vertices, length, area, centroid and OGC validity per document.

Geometry tab rendering four delivery-zone polygons on an SVG map with extent and vertex statistics

It reads from the document body rather than the {table}_spatial sidecar. The sidecar holds only bounding boxes for index pruning and its shape differs per provider, while the GeoJSON in Data is the actual value and is identical everywhere. So this works on every provider, including the ones with no spatial support at all. Parsing goes through the library’s own GeometryJsonConverter, so the tool inherits the real OGC model rather than reimplementing it.

Appears when the table has a {table}_blobs sidecar. Lists payloads without ever selecting the blob column, filterable by document id, with inline preview for images and text and delete behind a two-click confirm. Downloads stream over a plain HTTP endpoint rather than crossing the Blazor circuit as base64.

Stream a type out as JSON, NDJSON, envelope JSON (round-trippable) or CSV; import JSON or NDJSON back with fail / replace / skip handling for duplicate ids.

Import and export tab offering JSON array, NDJSON, CSV and envelope JSON exports

Whatever you type, in the target database’s dialect, with @name parameters bound from a JSON box so the types stay honest — 450 binds a number, "450" binds text.

SQL console running a parameterised aggregate query with its result grid

A documents table (using the provider’s own DDL) and a type (by writing its first document), so a database whose store has never run isn’t a dead end.

The volume holds saved connections, saved queries and uploaded database files; drop it and the UI starts empty every time.

Terminal window
docker run -p 8085:8080 -v shiny-docdb-myadmin:/data ghcr.io/shinyorg/shiny-docdb-myadmin

To reach a database file on your machine, mount its directory and use the container’s path in the connection string:

Terminal window
docker run -p 8085:8080 -v shiny-docdb-myadmin:/data \
-v /Users/me/databases:/databases:ro \
ghcr.io/shinyorg/shiny-docdb-myadmin

From a clone of the repo, either build the image or just run the project:

Terminal window
docker build -f src/ShinyDocDbMyAdmin/Dockerfile -t shiny-docdb-myadmin .
dotnet run --project src/ShinyDocDbMyAdmin

Shiny.DocumentDb.Aspire.Hosting models this tool as a resource, so it comes up with the rest of your app and every store you reference is already connected:

var store = builder.AddPostgresDocumentStore("orders");
builder.AddDocumentDbAdmin(port: 8085)
.WithReference(store)
.WaitFor(store);

WithReference is the same call a consuming service makes: the tool reads the ConnectionStrings:{name}

  • Shiny:DocumentDb:{name}:Provider pair the hosting integration already emits. A connection string with no matching provider key is ignored, so a Redis or blob reference in the same AppHost doesn’t turn into a junk connection. Referenced stores show up under a from host badge and can’t be edited or deleted from the UI — they’re declared in the AppHost, so that’s where they change.

It’s the same image either way — running the AppHost and publishing it — tagged to match the hosting package’s own version, so an integration upgrade brings the matching UI with it.

builder.AddDocumentDbAdmin(port: 8085)
.WithReference(store)
.WithDataVolume() // keep saved connections across runs
.WithHostPath("/Users/me/databases", "/databases") // make a file-backed store reachable
.WithSecretKey(builder.AddParameter("admin-key", secret: true))
.WithReadOnly() // for anything you didn't create
.WaitFor(store);

It’s a container, so it reaches the databases the AppHost models over the container network — which Aspire wires up for you. What it cannot reach is a file on your machine: a SQLite, SQLCipher or DuckDB store living at a host path is invisible inside the container unless you mount it, hence WithHostPath.

SettingEnvironment variableDefault
ShinyDocDbMyAdmin:DataDirectorySHINYDOCDBMYADMIN_DATA~/.shinydocdbmyadmin
ShinyDocDbMyAdmin:SecretKeySHINYDOCDBMYADMIN_KEYgenerated into <data>/secret.key
ShinyDocDbMyAdmin:ReadOnlyShinyDocDbMyAdmin__ReadOnlyfalse

The data directory holds the tool’s own SQLite document store (connection profiles and saved queries — dogfooding the library) plus any uploaded database files.

SecretKey encrypts the secret-bearing parts of a saved profile — connection strings and SQLCipher keys — with AES-GCM. Either a base64 32-byte key or a passphrase.

Connections handed over by a host are never written to that store at all — they live in memory for as long as the process does.

ReadOnly blocks every write path for every host-provided connection, which is the setting you want whenever the tool is pointed at something you didn’t create five minutes ago.

Mark a connection read-only and every write path is blocked, including non-SELECT statements in the SQL console. Deleting a connection or bulk-deleting documents takes two clicks. Passwords are masked wherever a connection string is displayed.

Blob bytes come from whoever wrote the document, so serving them from our own origin is a real XSS surface. Two rules keep it closed: every response carries nosniff and a default-src 'none' CSP, and anything not on the short raster-image allowlist is sent as an attachment rather than rendered. SVG is excluded from inline display on purpose — it’s a document format that can carry script, not merely an image.

Writes to a temporal-mapped type record a version, using the provider’s own history SQL and stamped with the actor shiny-docdb-myadmin. This is not optional bookkeeping: the admin layer writes SQL directly — it sits below IDocumentStore because it has no CLR type to bind to — so none of the library’s temporal tracking runs, and without it an edit made here would change the row while the history sidecar went on insisting the old body was current. A quietly wrong audit trail is worse than no History tab at all. Clear is the one exception, matching the library: a bulk delete is not tracked.