Skip to content

Architecture

Rewind is a single-user, local-only API client. It is built as a Bun workspace with three packages and two runnable services. This page is the map of the codebase.

Packages

text
.
├── apps/
│   ├── api/      @rewind/api   # Bun + Elysia HTTP server, request proxy, SQLite
│   └── web/      @rewind/web   # SvelteKit UI
└── docs/         @rewind/docs  # VitePress site (this documentation)

All three are workspaces under the root package.json. The root bun.lock is the single source of truth for dependencies; workspace packages do not have their own lockfiles. Shared versions (Elysia, Eden, Bun types, TypeScript) are pinned in the root workspaces.catalog.

Runtime services

Two services run side by side in development:

  • API at http://127.0.0.1:8000. Listens on the loopback interface by default. Binds to the loopback for safety — there is no authentication, and the proxy will relay any HTTP request the user can author.
  • Web app at http://localhost:5173. The SvelteKit dev server. A Vite proxy in apps/web/vite.config.ts forwards /api/* to the API so the web app can use relative URLs.

The release build fuses these into a single executable that serves the web bundle on the same port as the API.

Data flow

A request goes through this pipeline:

mermaid
graph LR
  UI[SvelteKit UI] -- Eden Treaty --> Elysia
  Elysia -- Elysia router --> Handler
  Handler -- SQLite --> DB[(SQLite)]
  Handler -- fetch --> Upstream[Upstream API]
  Handler -- storage paths --> FS[(Attachments / Response bodies / Logs)]
  Handler -- response --> Elysia
  Elysia -- typed response --> UI

Key points:

  • The web app never calls the upstream API directly. It always goes through the local proxy, which avoids CORS and lets the local proxy apply its own CORS allowlist.
  • The proxy persists every sent request and its response to the local database, so history is built up automatically.
  • Variables are resolved on the server, just before the upstream call. The client sends the unresolved draft; the server returns the resolved URL and the resolved headers.
  • Secrets are redacted before they touch the database. The redaction list lives in apps/api/src/services/request-runner.ts (redactPairs and redactDraft) and covers Authorization, Proxy-Authorization, Cookie, Set-Cookie, X-API-Key, and any pair whose key matches password|secret|token|authorization|cookie.

Backend modules

The API is organized around three concerns: a thin composition root, a set of core domain services, and a set of module route handlers. The whole package uses the @/ TypeScript path alias (configured in apps/api/tsconfig.json) so the relative-import chains are short.

The API lives under apps/api/src/:

Process & composition

  • index.ts — process bootstrap, signal handling, signal traps, and serving the bundled web build when one is present.
  • app.ts — the side-effect-free Elysia app factory. It instantiates the core services, wires up the HTTP middlewares, and use()s every modules/* route. Exports the App type that the web app imports.
  • config/config.tsloadConfig() and the AppConfig / ENV constants. Owns all env-var resolution and defaults (HOST, PORT, REWIND_DATA_DIR, SQLITE_PATH, WEB_BUILD_PATH, REWIND_OPEN_BROWSER, DEFAULT_APP_VERSION, DEFAULT_ALLOWED_ORIGINS).
  • runtime-paths.ts — resolves the data directory, the database path, the migrations directory, and the bundled-web path. Includes the one-time legacy migration from older apps/api/data/rewind-api-studio.sqlite workspaces.
  • cli.ts — the CLI runner entrypoint, exposed as bun run cli ….

Core services (core/<domain>/<domain>.service.ts)

Each domain owns its own service module. Services are plain factory functions: coreService(drizzle, …deps) → { …methods }. They are the only thing that talks to the database; route handlers never call Drizzle directly.

  • core/requests/ — collections, folders, saved requests, tree mutations.
  • core/env/ — environments and variables; activation, duplicate, lookup of the active environment's variable map.
  • core/history/ — history entries, response body storage, retention, listing/filtering.
  • core/execution/ — composing a draft, resolving variables, applying auth, sending through fetch, persisting to history. Owns the v1 (executeOnce) and v2 (executeStream) execution flows.
  • core/attachments/ — uploaded files: create, lookup, delete, garbage-collect.
  • core/cookies/ — the cookie jar: list, upsert from Set-Cookie, clear.
  • core/settings/ — single-row key/value settings + the typed network and storage settings.
  • core/workspace/ — full-workspace import/export, archive pack/unpack helpers.
  • core/oauth/ — in-memory OAuth token cache (keyed by tokenUrl + clientId + scope).
  • core/execution.types.ts, core/env.types.ts, core/history.types.ts, core/requests/request.types.ts, core/workspace/workspace.types.ts — the domain TypeScript types for each service. They are re-exported through types.tstypes/domain.ts so consumers can import from one place.

HTTP layer (http/)

  • http/response.ts — the shared error contract. Exports the ApiErrorCode union (validation_error, request_validation_error, not_found, forbidden, conflict, internal_error), the ApiErrorBody shape, and the notFound / errorEnvelope helpers. Every route uses this to produce error responses.
  • http/middleware/session.ts — CORS allowlist + the same-site rewind_session cookie that guards mutating requests. See Troubleshooting → Local session is not initialized.
  • http/middleware/logging.ts — request/response logging wired to the local logger.
  • http/middleware/errors.ts — uniform error response wrapping.

Route modules (modules/<resource>/index.ts)

One Elysia sub-app per resource, all with name: "rewind.<resource>". Each module file is intentionally thin: it parses the Elysia model, delegates to the matching core/* service, and returns the result. The app.ts composition root is the single place that wires the modules together.

  • modules/healthGET /health.
  • modules/workspaceGET /api/v1/workspace (the workspace summary used by the sidebar on boot).
  • modules/collection — collections, folders, and PATCH /api/v1/workspace/tree for bulk reordering.
  • modules/request — individual request CRUD: GET/PUT/DELETE /api/v1/requests/:id, POST /api/v1/requests/:id/duplicate, POST /api/v1/collections/:id/requests.
  • modules/execution — the actual sending pipeline: POST /api/v1/requests/send, POST /api/v2/requests/send, DELETE /api/v1/executions/:id, POST /api/v1/runner.
  • modules/environment — environment CRUD.
  • modules/historyGET/DELETE /api/v1/history, the v1/v2 GET /api/{v1,v2}/history/:id, and GET /api/v2/history/:id/body (downloads the stored response body).
  • modules/attachmentPOST /api/v1/attachments (multipart, max 100 MB), GET /api/v1/attachments/:id, DELETE /api/v1/attachments/:id.
  • modules/cookieGET /api/v1/cookies?jarId=…, DELETE /api/v1/cookies?jarId=….
  • modules/settingsGET/PUT /api/v1/settings/network, GET/PUT /api/v1/settings/storage.
  • modules/importerPOST /api/v1/import/{openapi,postman,workspace}, GET /api/v1/export/workspace, GET /api/v1/export/postman/:id.
  • modules/archiveGET /api/v2/workspace/archive, POST /api/v2/workspace/archive/{preview,restore}.
  • modules/diagnosticGET /api/v1/diagnostics/archive.

Each module has a sibling model.ts that exports an Elysia …ModelRegistry. The module's index.ts does .use(<Registry>) and then references the model names (body: "Request.SendInput", response: "Request.SendResponseV2", etc.) by string. Shared types (like Api.Error and Api.IdParams) live in modules/shared.model.ts and are registered where they are needed.

Persistence

  • db/schema.ts — Drizzle table definitions. The source of truth for the data model; see the Data model section below.
  • db/client.ts — the Drizzle client setup. Sets PRAGMA foreign_keys = ON, PRAGMA journal_mode = WAL, opens the SQLite file, and runs the Drizzle migrations from apps/api/drizzle/.
  • db/repository.ts — only the AttachmentReferenceError and RevisionConflictError error classes live here now. The typed methods that used to live here were moved into the per-domain services in core/*.

Services (services/)

Pure functions that the route modules call into. They don't know about HTTP.

  • services/request-runner.ts — variable resolution, auth application, body construction, the v1 runRequest function, and the redaction helpers (redactPairs, redactDraft).
  • services/openapi-importer.ts — OpenAPI 3 import.
  • services/postman-importer.ts — Postman v2.1 import and export.
  • services/workspace-archive.ts.rewind archive packing, unpacking, checksumming, the safety-backup flow, and the diagnostic archive.
  • services/logger.ts — re-exports the rolling text logger; the implementation lives in infra/logging/logger.ts.

Infra

  • infra/logging/logger.ts — the LocalLogger class. Rolling text log under <data-dir>/logs/rewind.log with redaction.

Lib

  • lib/http.ts, lib/variables.ts — small utilities used by the services and the request runner.

The App type is exported through the workspace package boundary as @rewind/api/app. The web app imports it through the workspace:* dependency, so the TypeScript types are inferred end-to-end without bundling any server runtime.

Frontend modules

The web app lives under apps/web/src/:

  • routes/+page.svelte — the workspace page that ties everything together.
  • routes/+layout.svelte and +layout.ts — the SvelteKit shell.
  • lib/api/client.ts — the typed Eden Treaty client. The exported api object is what the rest of the app uses.
  • lib/api/mappers.ts — maps between the API's DTOs and the in-memory draft types. Svelte 5 runes are not in the wire shapes; the mappers are the boundary.
  • lib/api/types.ts — Treaty-derived type aliases, kept in one place so other modules can import them without round-tripping through client.ts.
  • lib/api/index.ts — the barrel that re-exports api, ApiClientError, the Treaty types, and the mappers. Most of the app imports from here.
  • lib/state/*.svelte.ts — Svelte 5 runes-based state. One file per concern: collections, documents, environment, request, response. request.svelte.ts owns the runes state; the non-runes draft types and constructors live in the same file (the older state/request.ts has been folded in).
  • lib/components/*.svelte — UI components. The biggest ones are sidebar, request-panel, response-panel, url-bar, body-editor, and top-bar. The new MethodBadge component is the color-coded method label used in tabs, the request panel, the sidebar, and the history list.
  • lib/components/ui/* — the local component library (button, input, dialog, sheet, tabs, table, etc.), each in its own folder.
  • lib/theme/tokens.css and lib/theme/theme.svelte.ts — the design tokens (surface, subtle-foreground, per-method colors) and the theme toggle.
  • lib/utils/*.ts — small utilities (formatting, code generation, HTTP helpers).

Data model

The schema is the source of truth for the data model. The ten tables are:

  • collections — a top-level group
  • folders — a sub-grouping inside a collection (or another folder), with a self-referential parentId
  • requests — a saved request, with method, url, and JSON-encoded headers, queryParams, body, auth
  • environments — a named variable bundle, with exactly one is_active = 1
  • environment_variables — the variables inside an environment, with a unique key per environment
  • history — every sent request and its outcome
  • response_bodies — the on-disk location of every stored response body, with a storage_state of stored / omitted_limit / missing
  • settings — single-row key/value store for network, storage, and other global settings
  • attachments — uploaded files, with the file's path on disk
  • cookies — the cookie jar, keyed by (jarId, domain, path, name)

The schema is in apps/api/src/db/schema.ts; generated SQL migrations are in apps/api/drizzle/. After editing the schema, run bun run db:generate to produce a new migration.

Why a local backend at all

A browser cannot reliably send requests to arbitrary APIs because CORS blocks many targets. The local proxy also lets Rewind:

  • Read the local cookie jar, response bodies, and attachments without going through the browser's storage.
  • Apply its own CORS allowlist so the local API only serves the local UI.
  • Persist every request to the local database for history.
  • Resolve {{variables}} consistently between the URL bar and the actual upstream call.

The local backend is the simplest design that meets these requirements. It binds to 127.0.0.1 and is intended for local single-user development.

What's next?

Released under the MIT License.