Problem / Motivation
When a backup job covers several databases from one source, the result is a single unified TAR archive. Today the only way to get one database out of it is POST /api/storage/{id}/download-url, which returns a token for the entire archive, decrypted. There is no way to fetch just one dump.
Two consequences:
1. Disproportionate transfer for a routine task. Anyone running a multi-database job who needs a single dump has to pull and decrypt the whole archive - tens of GB moved to obtain a few hundred MB. On a busy shared database server this is the normal case, not an edge case.
2. It makes delegated access impossible. We are an IT company managing servers for clients and resellers. Some clients own a whole server; many own a single site, and therefore a single database, on a shared server - often with an external developer maintaining it. Those developers regularly need a dump before a deploy or to reproduce something locally, and today every one of those requests goes through us by hand (currently: mount the Restic repo, extract, put the file somewhere).
We would like to put a small portal in front of the DBackup API so they can serve themselves. The restore half already works: POST /api/storage/{id}/restore accepts a databaseMapping array with per-entry selected flags, so restoring exactly one database out of a shared archive is expressible today. The download half is the blocker - handing a client the whole archive would hand them every other client's data on that server.
Splitting into one job per database would solve it on paper, but we manage a large number of servers and most are not client-specific, so that multiplies job count across the whole estate to solve a problem that affects only a subset of databases.
Proposed Solution
Expose the per-database selection that the archive format already supports.
Preferred shape - extend the existing endpoint. POST /api/storage/{id}/restore-files already streams a selection as a gzipped tar to target: {kind: "download"}, and already mints single-use tokens via prepare: true. Adding a database dimension to its schema would cover this without a new surface:
Alternative shape - a dedicated endpoint mirroring download-url, if that is cleaner:
In the UI, the natural home is the restore dialog: a "Download dump" action per database, matching the Download selection (.tar.gz) that directory sources already have.
On permissions, storage:download would be the intuitive gate. It would be genuinely useful if this worked without storage:restore, so a download-only integration does not need a key that can also write to databases. (Note that analyze and browse-archive currently require storage:restore, which makes a read-only integration hard to build.)
Why this may be smaller than it looks
Reading main at the time of writing, most of the mechanism appears to exist already:
IndexDatabaseLine (k: "db" in src/lib/archive/types.ts) maps each dump 1:1 onto a physical entry with a byte offset and size, and v2 archives are seekable - so a single dump is addressable without reading the rest.
ArchiveSelection already declares databaseNames?: string[].
src/lib/archive/extract.ts already filters index.databases against selection.databaseNames - this is what a selective restore uses.
restore-files already handles streaming a selection to a download target, including the single-use token path.
The gap seems to be that src/services/restore/file-restore.ts and the restore-files Zod schema are directory-oriented and have no notion of db entries. Apologies if this reading is off - it is from the public source, not from running it.
Alternatives Considered
- One backup job per database. Correct in isolation, but our servers host many clients' databases; this would multiply jobs across the whole estate to serve the subset that actually needs delegated access.
- Download the full archive in our own middleware, extract one dump, serve that. Possible today, but it transfers and decrypts the entire archive per request, and means our portal temporarily holds every client's plaintext data. That is precisely the exposure we are trying to design away.
- Restore to a scratch server, then dump from there. Extra infrastructure, an extra copy of client data at rest, and slow.
- Keep our existing Restic pipeline and build the portal against that instead. Restic gives us the file but not restore-into-a-database, which is the part of DBackup we actually want.
Area
API
Additional Context
Where we would eventually like to get to. The endpoint above unblocks us on its own - we are happy to build the client mapping and authorization layer in our own application. But the direction we are heading is a client-facing view with multi-level access: a client, reseller, or their developer linked to specific databases and/or servers, able to list, download, and where appropriate restore only those, with restore targets pinned by us. Access granularity down to a single database on a shared server is the requirement, because that is how a lot of our hosting actually looks - some clients have a whole server, others just one site on a shared one, maintained by their own developer.
We understand that is a data-model change and not something to ask for casually. If a smaller primitive were ever on the table, generic labels/tags on sources, jobs and databases would go a long way: we could attach our own client mapping without DBackup needing to know what a client is, and other users would get environment, team, or criticality tagging out of it. Label-scoped permissions would then be a natural later step. Not part of this request - just context for where a per-database download would lead.
Today's permission model is type-level (storage:restore grants restore on everything), so all scoping has to live in our middleware regardless. That is a trade-off we accept; the single-database download is the one piece we cannot work around.
Related: #137.
Happy to test against a branch or help however is useful.
Problem / Motivation
When a backup job covers several databases from one source, the result is a single unified TAR archive. Today the only way to get one database out of it is
POST /api/storage/{id}/download-url, which returns a token for the entire archive, decrypted. There is no way to fetch just one dump.Two consequences:
1. Disproportionate transfer for a routine task. Anyone running a multi-database job who needs a single dump has to pull and decrypt the whole archive - tens of GB moved to obtain a few hundred MB. On a busy shared database server this is the normal case, not an edge case.
2. It makes delegated access impossible. We are an IT company managing servers for clients and resellers. Some clients own a whole server; many own a single site, and therefore a single database, on a shared server - often with an external developer maintaining it. Those developers regularly need a dump before a deploy or to reproduce something locally, and today every one of those requests goes through us by hand (currently: mount the Restic repo, extract, put the file somewhere).
We would like to put a small portal in front of the DBackup API so they can serve themselves. The restore half already works:
POST /api/storage/{id}/restoreaccepts adatabaseMappingarray with per-entryselectedflags, so restoring exactly one database out of a shared archive is expressible today. The download half is the blocker - handing a client the whole archive would hand them every other client's data on that server.Splitting into one job per database would solve it on paper, but we manage a large number of servers and most are not client-specific, so that multiplies job count across the whole estate to solve a problem that affects only a subset of databases.
Proposed Solution
Expose the per-database selection that the archive format already supports.
Preferred shape - extend the existing endpoint.
POST /api/storage/{id}/restore-filesalready streams a selection as a gzipped tar totarget: {kind: "download"}, and already mints single-use tokens viaprepare: true. Adding a database dimension to its schema would cover this without a new surface:POST /api/storage/{id}/restore-files { "file": "backups/shared-mysql-01/2026-08-02.tar", "databases": ["customer_shop_prod"], // new, alongside the existing `selections` "target": { "kind": "download" } }Alternative shape - a dedicated endpoint mirroring
download-url, if that is cleaner:POST /api/storage/{id}/download-database { "file": "...", "database": "customer_shop_prod", "decrypt": true } -> { "url": "...", "expiresIn": "5 minutes", "singleUse": true }In the UI, the natural home is the restore dialog: a "Download dump" action per database, matching the Download selection (.tar.gz) that directory sources already have.
On permissions,
storage:downloadwould be the intuitive gate. It would be genuinely useful if this worked withoutstorage:restore, so a download-only integration does not need a key that can also write to databases. (Note thatanalyzeandbrowse-archivecurrently requirestorage:restore, which makes a read-only integration hard to build.)Why this may be smaller than it looks
Reading
mainat the time of writing, most of the mechanism appears to exist already:IndexDatabaseLine(k: "db"insrc/lib/archive/types.ts) maps each dump 1:1 onto a physical entry with a byte offset and size, and v2 archives are seekable - so a single dump is addressable without reading the rest.ArchiveSelectionalready declaresdatabaseNames?: string[].src/lib/archive/extract.tsalready filtersindex.databasesagainstselection.databaseNames- this is what a selective restore uses.restore-filesalready handles streaming a selection to a download target, including the single-use token path.The gap seems to be that
src/services/restore/file-restore.tsand therestore-filesZod schema are directory-oriented and have no notion ofdbentries. Apologies if this reading is off - it is from the public source, not from running it.Alternatives Considered
Area
API
Additional Context
Where we would eventually like to get to. The endpoint above unblocks us on its own - we are happy to build the client mapping and authorization layer in our own application. But the direction we are heading is a client-facing view with multi-level access: a client, reseller, or their developer linked to specific databases and/or servers, able to list, download, and where appropriate restore only those, with restore targets pinned by us. Access granularity down to a single database on a shared server is the requirement, because that is how a lot of our hosting actually looks - some clients have a whole server, others just one site on a shared one, maintained by their own developer.
We understand that is a data-model change and not something to ask for casually. If a smaller primitive were ever on the table, generic labels/tags on sources, jobs and databases would go a long way: we could attach our own client mapping without DBackup needing to know what a client is, and other users would get environment, team, or criticality tagging out of it. Label-scoped permissions would then be a natural later step. Not part of this request - just context for where a per-database download would lead.
Today's permission model is type-level (
storage:restoregrants restore on everything), so all scoping has to live in our middleware regardless. That is a trade-off we accept; the single-database download is the one piece we cannot work around.Related: #137.
Happy to test against a branch or help however is useful.