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.
docker run -p 8085:8080 -v shiny-docdb-myadmin:/data ghcr.io/shinyorg/shiny-docdb-myadminSupported providers
Section titled “Supported providers”Every relational DocumentDb backend:
| Provider | Connection | Notes |
|---|---|---|
| SQLite | file, uploadable | |
| SQLCipher | file, uploadable | needs the encryption key |
| DuckDB | file, uploadable | takes an exclusive lock while open |
| PostgreSQL | server | |
| SQL Server | server | |
| MySQL | server | |
| MariaDB | server | |
| Oracle 23ai+ | server | |
| CockroachDB | server |
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.
Explorer
Section titled “Explorer”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.
Browse
Section titled “Browse”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".
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.
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.
Structure
Section titled “Structure”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.
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.
History
Section titled “History”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.
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.
Geometry
Section titled “Geometry”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.
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.
Import / Export
Section titled “Import / Export”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.
SQL console
Section titled “SQL console”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.
Create
Section titled “Create”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.
Running it
Section titled “Running it”The volume holds saved connections, saved queries and uploaded database files; drop it and the UI starts empty every time.
docker run -p 8085:8080 -v shiny-docdb-myadmin:/data ghcr.io/shinyorg/shiny-docdb-myadminTo reach a database file on your machine, mount its directory and use the container’s path in the connection string:
docker run -p 8085:8080 -v shiny-docdb-myadmin:/data \ -v /Users/me/databases:/databases:ro \ ghcr.io/shinyorg/shiny-docdb-myadminFrom a clone of the repo, either build the image or just run the project:
docker build -f src/ShinyDocDbMyAdmin/Dockerfile -t shiny-docdb-myadmin .dotnet run --project src/ShinyDocDbMyAdminIn an Aspire AppHost
Section titled “In an Aspire AppHost”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}:Providerpair 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.
Configuration
Section titled “Configuration”| Setting | Environment variable | Default |
|---|---|---|
ShinyDocDbMyAdmin:DataDirectory | SHINYDOCDBMYADMIN_DATA | ~/.shinydocdbmyadmin |
ShinyDocDbMyAdmin:SecretKey | SHINYDOCDBMYADMIN_KEY | generated into <data>/secret.key |
ShinyDocDbMyAdmin:ReadOnly | ShinyDocDbMyAdmin__ReadOnly | false |
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.
Safety
Section titled “Safety”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.