Skip to content

feat(workspace): owner-downloadable full-workspace zip export (#320)#213

Merged
ikrasovytskyi merged 1 commit into
mainfrom
web/320-workspace-export
Jul 25, 2026
Merged

feat(workspace): owner-downloadable full-workspace zip export (#320)#213
ikrasovytskyi merged 1 commit into
mainfrom
web/320-workspace-export

Conversation

@ikrasovytskyi

Copy link
Copy Markdown
Contributor

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:

GET /api/v1/workspaces/:ws/export/workspace.zip

@UseGuards(AuthGuard, WorkspaceAccessGuard) + @MinRole('admin') (a regular member/guest gets 403) + @RequiresScope('admin') for API tokens. An unknown workspace 404s (never leaks existence).

Format / folder layout

Documented in docs/architecture/workspace-export.md. Layout:

manifest.json                          # workspace meta, format version (1), timestamp, index of spaces/databases
spaces/<space-slug>/<database-slug>.json   # field SCHEMA (types + config + select options) + all records
relations.json                         # relation defs + links, by stable record id
attachments/<attachment-id>/<filename> # the actual attachment files
  • Records carry values keyed by field id (the stable uuid, raw stored form — ADR-0002), plus title, number, timestamps, and an attachments[] array whose path points into attachments/.
  • Relations are reconstructable from stable record ids: relations.json has the relation definitions (schema edges) and every { relation_id, from_record_id, to_record_id } link.
  • Select values stay resolvable because each field's options[] are exported alongside it.
  • Archive paths are sanitised (no separators/control chars/..) and de-duplicated — zip-slip safe.

A future import is intentionally left possible (stable uuid keys, schema-with-data, a format_version to gate on) but not built now.

Streaming

The .zip is built lazily with archiver: 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

  • Backend integration test (apps/api/test/workspace-export.test.ts): admin can export; a non-admin member is refused (403); unknown workspace 404s; the zip is unzipped in-test and asserted — manifest.json indexes the databases, each database file carries its field schema (incl. select options) and records with values, relations.json resolves the link to real record ids, and the attachment bytes round-trip byte-for-byte (local-disk storage driver).
  • Full local CI green: lint, typecheck, test (1591 passed / 1 skipped), build, docs:check, pnpm install --frozen-lockfile. SDK regenerated + committed for the new endpoint.
  • Interactive browser check NOT done — blocked by known issue #355 (better-auth useSession hang 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/export built).

Follow-ups (not in this PR)

  • Async/background export job for very large workspaces — v1 is a synchronous stream, which is acceptable but a background job (with a downloadable artifact) is the sensible next step.
  • Broaden scope in a future format_version: views, standalone space documents, record rich-text document bodies, comments, activity history.

Closes #320.

🤖 Generated with Claude Code

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
ikrasovytskyi enabled auto-merge July 25, 2026 21:03
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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

@ikrasovytskyi
ikrasovytskyi added this pull request to the merge queue Jul 25, 2026
Merged via the queue into main with commit 1f0201f Jul 25, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant