Appearance
Server API ​
Nitro server routes live in server/. They exist for one core reason: secrets — AWS credentials, the Typesense admin key, backend API tokens — must never reach the client bundle. All external service calls that require those credentials go through these routes. The routes also provide a consistent place to enforce authentication, rate limiting, and CSRF protection before touching any sensitive operation.
Route inventory ​
server/
├── api/
│ ├── assets.get.ts
│ ├── browser-os.get.ts
│ ├── csrf-token.get.ts
│ ├── image-proxy.get.ts
│ ├── resolution.post.ts
│ ├── typesense-collection.post.ts # legacy path
│ ├── typesense-ids.post.ts # legacy path
│ ├── upload-asset.post.ts
│ ├── s3/
│ │ ├── bucket-config.get.ts
│ │ ├── bucket-config.post.ts
│ │ ├── complete-upload.post.ts
│ │ ├── delete-asset.delete.ts
│ │ ├── download-file.get.ts
│ │ ├── get-display-file.get.ts
│ │ ├── get-signed-url.get.ts
│ │ ├── get-upload-url.get.ts
│ │ ├── resource.get.ts
│ │ ├── start-upload.get.ts
│ │ └── upload.post.ts
│ ├── typesense/
│ │ ├── collections-count.post.ts
│ │ ├── search-collection.post.ts
│ │ ├── search-ids.post.ts
│ │ └── search.post.ts
│ └── zip-viewer/
│ └── extract.post.ts
└── routes/
├── apple-app-site-association.get.ts
├── health.get.ts
├── manifest.json.get.ts
└── preview_assets.get.tsCSRF ​
| Method | Route | Description |
|---|---|---|
| GET | /api/csrf-token | Generates a cryptographically random CSRF token, writes it to the XSRF-TOKEN cookie (JS-accessible, 24h), and returns it in the response body. Called once on app load. Implements the Double-Submit Cookie pattern. |
The client reads the cookie and sends it as X-XSRF-TOKEN on mutating requests. requireCsrfToken() in server/utils/csrf.ts validates the match.
Typesense routes (server/api/typesense/) ​
The Typesense API key is server-only. These routes proxy search requests, enforce workspace-scoped filtering, and presign any raw S3 keys in results before responding.
| Method | Route | Description |
|---|---|---|
| POST | /api/typesense/search | Main search proxy. Handles three body shapes: a single-collection search, a multi-search (searches array), and a legacy compat format (request + commonSearchParams). Validates input with Zod, verifies workspace access, composes workspace_id filter, and presigns S3 paths in all hits. |
| POST | /api/typesense/search-collection | Retrieves a single document by collection + id. Presigns S3 paths in the returned doc. Enforces a workspace tenant guard (403 if the doc's workspace_id doesn't match). |
| POST | /api/typesense/search-ids | Fetches every matching document ID across one or more collections. Paginates at 10,000 results per page. Used by the select-all-across-pages flow for bulk downloads, moves, and shares. Returns Record<collection, id[]>. |
| POST | /api/typesense/collections-count | Returns the document count for each of the four DAM collections (digital_assets, digital_assets_categories, dam_collections, digital_assets_tags) plus a total. |
Legacy Typesense routes — the two files at server/api/typesense-collection.post.ts and server/api/typesense-ids.post.ts are older implementations of /api/typesense/search-collection and /api/typesense/search-ids respectively. They use a different internal path but serve the same purpose. Prefer the versioned paths under server/api/typesense/.
S3 routes (server/api/s3/) ​
AWS credentials never leave the server. These routes handle the full multipart upload lifecycle, presigned URL generation, object deletion, and resource fetching.
| Method | Route | Description |
|---|---|---|
| GET | /api/s3/start-upload | Initiates a multipart S3 upload via CreateMultipartUploadCommand. Returns { uploadId, assetId } where assetId is a server-generated UUID used as the S3 object key. |
| GET | /api/s3/get-upload-url | Returns a presigned UploadPart URL for one chunk of a multipart upload. Query params: assetId, PartNumber, UploadId, workspaceId. |
| POST | /api/s3/complete-upload | Finalises a multipart upload via CompleteMultipartUploadCommand. Body: { assetId, UploadId, Parts, workspaceId }. |
| DELETE | /api/s3/delete-asset | Deletes an object at {workspaceId}/digital_assets/{assetId}. Body: { assetId, workspaceId }. |
| GET | /api/s3/get-signed-url | Returns a 24h presigned GET URL for {workspaceId}/digital_assets/{assetId}. Enforces alphanumeric-only assetId to block path traversal. |
| GET | /api/s3/get-display-file | Same as get-signed-url (24h presigned GET URL) but without the strict assetId format check. Used for display/preview contexts. |
| POST | /api/s3/upload | Single-part upload via PutObjectCommand. Accepts multipart form data. Query params: workspaceId, optional compress, key (override the generated UUID), location (override the key prefix), isPublic (set public-read ACL). Returns { assetId, signedUrl }. |
| GET | /api/s3/download-file | Proxies a file from a given url query param and streams it back as an attachment (Content-Disposition: attachment). No authentication required. |
| GET | /api/s3/resource | Fetches any external URL, encodes the response as a base64 data URI (data:<contentType>;base64,<data>), and returns { data }. Validates the URL against SSRF (blocks localhost, private IPs, cloud metadata endpoints). Auth required. |
| GET | /api/s3/bucket-config | Returns the current CORS rules on the configured S3 bucket. Auth required. |
| POST | /api/s3/bucket-config | Updates the S3 bucket CORS rules using the CORS_ALLOWED_ORIGINS environment variable. Auth required. |
Asset and upload routes ​
| Method | Route | Description |
|---|---|---|
| GET | /api/assets | Proxies asset download metadata from the backend (/digital-assets/object-download). Query params: assets_id, workspace_id, optional attachment_type. Auth required. |
| POST | /api/upload-asset | Orchestrates a full multipart upload end-to-end: validates the file, calls s3/start-upload, s3/get-upload-url for each 10MB chunk, uploads each part directly to S3, calls s3/complete-upload, fetches a signed URL, and registers the asset with the backend API (/digital-assets/upload). If asset_id is provided, registers a new version instead (/digital-assets/version/upload). Rate limited. |
Image and media routes ​
| Method | Route | Description |
|---|---|---|
| GET | /api/image-proxy | Proxies external images. Validates the url query param against SSRF (blocks private IPs, localhost, cloud metadata). Validates the upstream Content-Type is an image. Rate limited (10 req/min). Auth required. |
| POST | /api/resolution | Resizes an image using Jimp. Body: { w, h, s, q, m } where s is a URL or base64 string, q is JPEG quality, and m is the output MIME type. Caps at 600MP and 200MB source. Returns the resized image buffer. |
Zip viewer ​
| Method | Route | Description |
|---|---|---|
| POST | /api/zip-viewer/extract | Accepts a zip file upload (multipart, max 100MB). Parses it with adm-zip and returns the first-level hierarchy as Array<{ name, directory? }>. CSRF token required, rate limited. |
Utility routes ​
| Method | Route | Description |
|---|---|---|
| GET | /api/browser-os | Returns geo-location data (via the geolocation-db.com third-party API, degrades to null on failure) and parsed user-agent info ({ isMobile, isTablet, isDesktop, browser, version, os, platform }). Used for analytics and download tracking. |
Server routes (server/routes/) ​
These are served at the root path (not under /api).
| Method | Route | Description |
|---|---|---|
| GET | /health | Dependency health check. Probes backend API reachability (3s timeout). Returns `{ status: 'ok' |
| GET | /manifest.json | Dynamic PWA manifest. White-label hosts get their brand name and favicon from the backend check-branding endpoint; all others get the default Collage manifest. |
| GET | /apple-app-site-association | iOS Universal Links JSON. Served with Content-Type: application/json at the exact extensionless path Apple requires. |
| GET | /preview_assets | Authenticated redirect proxy. Forwards query params to the backend preview-image endpoint, validates the returned URL is HTTPS, and issues a 303 redirect. |
Server utils (server/utils/) ​
| File | Key exports | Purpose |
|---|---|---|
csrf.ts | generateCsrfToken, setCsrfTokenCookie, requireCsrfToken, validateCsrfToken | Double-Submit Cookie CSRF implementation. requireCsrfToken throws 403 on mutating requests when the XSRF-TOKEN cookie and X-XSRF-TOKEN header don't match. |
rateLimit.ts | requireRateLimit, checkRateLimit | In-memory rate limiter keyed by auth token or IP. Per-route limits: s3/upload and image-proxy at 10/min, everything else at 100/min. Sets standard X-RateLimit-* response headers. |
signSearchUrls.ts | signSearchResponse, signSingleDoc, readSignerConfig | Presigns raw S3 keys ({workspaceId}/digital_assets/...) found in Typesense hits. Maintains a 23h in-memory LRU cache (max 10,000 entries) to avoid re-signing the same key on every search. Used by the typesense/search and typesense-collection routes. |
typesense.ts | useTypesense, useS3, getCachedSignedUrl, verifyAndResolveWorkspace, verifyWorkspaceAccess, buildFilter, mapSortBy, signS3PathsInResults | Typesense and S3 client factories, workspace authentication (validates the caller's JWT against the backend /user endpoint and checks workspace membership), filter composition utilities, and S3 URL signing. verifyWorkspaceAccess caches successful auth results for 10s (keyed by SHA-256 of token + workspace ID) to reduce /user calls on high-frequency search paths. |
s3/authHelper.ts | getApiBaseUrl, verifyAuthentication, validateWorkspaceId | Resolves the backend API base URL from runtime config, verifies a bearer token against the backend /user endpoint (5s timeout), and validates workspace ID format. |
s3/fileValidation.ts | validateFile, validateFileSize, validateMimeType, validateFileExtension, validateFileContent, sanitizeFilename, validateLocation | Upload security: enforces 100MB max file size, MIME type and extension allowlisting, magic-number content validation for known types, filename sanitisation (path traversal prevention, 255 char limit), and location parameter sanitisation. |
s3/utils.ts | s3, getS3Client, getS3Bucket, toStream, uuidv4 | Lazy-initialised AWS S3Client factory. s3 is a Proxy for backward-compat with code that references the client directly. toStream converts a Buffer to a Readable stream for SDK uploads. |
Security notes ​
- All S3 and Typesense routes validate the caller's bearer token against the backend before acting.
- Workspace-scoped operations (
s3/*,typesense/*) additionally verify the caller is a member of the requested workspace. - Mutating operations that use CSRF validation are those that go through
requireCsrfToken— currentlyzip-viewer/extract. - The
image-proxyands3/resourceroutes both block SSRF: private IP ranges, loopback, and cloud metadata endpoints (169.254.169.254,metadata.google.internal,metadata.azure.com) are rejected before any outbound request is made. - File uploads validate size, MIME type, extension, and magic-number content signature. Filenames are sanitised before use.
- S3 presigned URLs expire in 24 hours. The server-side cache for signed Typesense results uses a 23h TTL to stay safely within that window.
Related documentation ​
- Architecture — Nitro server layer
- Configuration —
runtimeConfigfor server secrets (AWS_*,TYPESENSE_*,API_BASE_URL)