feat(workspace): owner-downloadable full-workspace zip export (#320)#213
Merged
Conversation
StoryOS locks nothing in, so a workspace owner can take the entire
workspace out in one download: spaces, databases, the field schema
(types + config + select options), every record with its raw values,
the relation graph, and the attachment bytes.
- New admin-only endpoint GET /workspaces/:ws/export/workspace.zip
(@MinRole('admin') + @RequiresScope('admin')), streamed as a .zip so a
large workspace is never buffered into memory. Record pages and
attachment bytes are pulled one at a time as the response is read.
- Attachments flow through the shared storage seam (getStorage(),
MN-029), so the export works identically on local-disk and S3/MinIO.
- Archive layout (docs/architecture/workspace-export.md):
manifest.json - meta, format version, index
spaces/<space>/<database>.json - schema + records (values by field id)
relations.json - relation defs + links, by record id
attachments/<attachment-id>/<filename> - the files
Records/relations key on stable uuids so the graph is reconstructable;
archive paths are sanitised + de-duplicated (zip-slip safe).
- Web: an admin-only "Export" section in workspace settings with a
streaming download link.
- No import yet (documented as a future format_version concern); no
migration.
Verified: api integration test (owner exports; non-admin 403; unknown
workspace 404; manifest + per-database schema/records + relations +
attachment bytes round-trip, unzipped in-test). Full local CI green
(lint, typecheck, 1591 tests, build, docs:check, frozen install).
Interactive browser check blocked by #355 (better-auth split-origin
dev hang / worktree .env) — endpoint verified via tests instead.
Signed-off-by: ikrasovytskyi <i.krasovytskyi@owox.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
ikrasovytskyi
enabled auto-merge
July 25, 2026 21:03
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
storyos-docs | 02c98e3 | Commit Preview URL Branch Preview URL |
Jul 25 2026, 09:05 PM |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
StoryOS is a no-lock-in, Fibery-like tool — so a workspace owner should be able to take everything out. This adds an owner-facing full-workspace export as a single streamed
.zip(#320).Access control
Admin/owner only, gated exactly like the other owner-level actions:
@UseGuards(AuthGuard, WorkspaceAccessGuard)+@MinRole('admin')(a regular member/guest gets403) +@RequiresScope('admin')for API tokens. An unknown workspace404s (never leaks existence).Format / folder layout
Documented in
docs/architecture/workspace-export.md. Layout:valueskeyed by field id (the stable uuid, raw stored form — ADR-0002), plustitle,number, timestamps, and anattachments[]array whosepathpoints intoattachments/.relations.jsonhas the relation definitions (schema edges) and every{ relation_id, from_record_id, to_record_id }link.options[]are exported alongside it...) and de-duplicated — zip-slip safe.A future import is intentionally left possible (stable uuid keys, schema-with-data, a
format_versionto gate on) but not built now.Streaming
The
.zipis built lazily witharchiver: per-database JSON is streamed page-by-page (500 records at a time) and attachment bytes are pulled one at a time as the HTTP response is read — a large workspace is never buffered into memory. Structural metadata (spaces/databases/fields/relations/attachment metadata) is read eagerly; only record bytes and file bytes stream.Attachments via the storage seam
Files are read through the shared
getStorage()abstraction (MN-029), so the export works identically on a local-disk (ATTACHMENTS_DIR) or an S3/MinIO (STORAGE_DRIVER=s3) deployment. A missing object (deleted underneath the export) is skipped rather than aborting the whole archive.Web
An admin-only Export section in workspace settings (
/w/:ws/settings/export) with a streaming download link (a plain<a href>, credentials via cookie — same approach as the existing CSV export).Migration
None.
Verified how
apps/api/test/workspace-export.test.ts): admin can export; a non-admin member is refused (403); unknown workspace404s; the zip is unzipped in-test and asserted —manifest.jsonindexes the databases, each database file carries its field schema (incl. select options) and records with values,relations.jsonresolves the link to real record ids, and the attachment bytes round-trip byte-for-byte (local-disk storage driver).lint,typecheck,test(1591 passed / 1 skipped),build,docs:check,pnpm install --frozen-lockfile. SDK regenerated + committed for the new endpoint.useSessionhang in split-origin dev; worktree lacks.env). Not fabricating a screenshot; the endpoint is covered by the integration test and the web bundle compiles (route/w/[ws]/settings/exportbuilt).Follow-ups (not in this PR)
format_version: views, standalone space documents, record rich-text document bodies, comments, activity history.Closes #320.
🤖 Generated with Claude Code