API Reference
The fla-- application exposes a RESTful API under /api/ for file management and authentication. All endpoints require a valid session unless stated otherwise. The API is implemented as SolidStart server routes in src/routes/api/.
Authentication
All API routes are protected by Better-Auth session validation. Requests without a valid session receive a 401 Unauthorized response.
POST /api/auth/* — Better-Auth catch-all (login, logout, register, session, callback)
The auth handler is a pass-through to Better-Auth via toSolidStartHandler(auth), which provides standard endpoints for sign-in, sign-up, session management, and OAuth callbacks.
File Management API
GET /api/files
List all files owned by the authenticated user, including each file's latest version metadata.
Response 200 OK — Array of file objects:
[
{
"id": "uuid",
"filename": "my-machine.json",
"created_at": "2026-09-10T12:00:00Z",
"updated_at": "2026-09-14T08:30:00Z",
"version_number": 3,
"mime_type": "application/json",
"size_bytes": 2048
}
]
Ordered by updated_at descending (most recently modified first).
POST /api/files
Create a new file (version 1). Expects multipart/form-data with a file field.
Request:
Content-Type: multipart/form-data- Body field:
file(binary file)
Constraints:
- File must not be empty (
400 Bad Request) - File must not exceed 100 MB (
413 Payload Too Large)
Response 201 Created — The created file record merged with its version 1 metadata.
Server-side flow:
- Uploads file bytes to the external file storage service under a UUID-named path
- Inserts a row into
user_files - Inserts a row into
file_versionswithversion_number = 1 - Both inserts are wrapped in a database transaction
GET /api/files/:fileId
Download the latest version of a file. Readable by any signed-in user (supports shared links); writes remain owner-only.
Response 200 OK — File bytes with headers:
| Header | Description |
|---|---|
Content-Type | MIME type of the stored file |
Content-Disposition | attachment; filename="<original-name>" |
X-Version-Number | Version number of the returned content |
X-Owned | "true" if the requester owns the file, "false" otherwise |
Returns 404 Not Found if the file or its latest version does not exist.
PUT /api/files/:fileId
Save a new version of an existing file. Owner-only. Expects multipart/form-data with a file field.
Request:
Content-Type: multipart/form-data- Body field:
file(binary file)
Constraints:
- File must exist and be owned by the requester (
404 Not Found) - File must not be empty (
400 Bad Request) - File must not exceed 100 MB (
413 Payload Too Large)
Response 200 OK — The new file_versions row.
Server-side flow:
- Uploads new file bytes to storage under a fresh UUID path (old version bytes remain untouched)
- Computes
nextVersion = MAX(version_number) + 1for the file - Inserts the new version row
- Updates
user_files.updated_at - Prunes versions beyond the retention limit (keeps the latest 4) — deletes DB rows first, then best-effort deletes storage bytes after commit
- All database operations wrapped in a transaction
PATCH /api/files/:fileId
Rename a file. Owner-only. Expects a JSON body.
Request:
Content-Type: application/json- Body:
{ "filename": "new-name.json" }
Constraints:
- Filename must be a non-empty string (
400 Bad Request) - Filename must not exceed 255 characters (
400 Bad Request) - File must exist and be owned by the requester (
404 Not Found)
Response 200 OK — The updated user_files row.
DELETE /api/files/:fileId
Delete a file and all its versions. Owner-only. Removes both database records and storage bytes.
Response 204 No Content on success.
Server-side flow:
- Fetches all version
storage_pathvalues for the file - Deletes each version's bytes from the external file storage service
- Deletes the
user_filesrow (cascades tofile_versionsvia foreign key)
GET /api/files/:fileId/versions
List all versions of a file, newest first. Owner-only.
Response 200 OK — Array of version objects:
[
{
"id": "uuid",
"version_number": 3,
"mime_type": "application/json",
"size_bytes": 2048,
"created_at": "2026-09-14T08:30:00Z"
},
{
"id": "uuid",
"version_number": 2,
"mime_type": "application/json",
"size_bytes": 1900,
"created_at": "2026-09-12T15:00:00Z"
}
]
Returns 404 Not Found if the file does not exist or is not owned by the requester.
GET /api/files/:fileId/versions/:versionId
Download a specific version of a file. Owner-only.
Response 200 OK — File bytes with headers:
| Header | Description |
|---|---|
Content-Type | MIME type of the stored file |
Content-Disposition | attachment; filename="<original-name>" |
X-Version-Number | Version number of the returned content |
Returns 404 Not Found if the version does not exist or the file is not owned by the requester.
Endpoint Summary
| Method | Endpoint | Auth | Owner-only | Description |
|---|---|---|---|---|
GET | /api/files | Yes | Yes | List user's files with latest version info |
POST | /api/files | Yes | Yes | Create a new file (version 1) |
GET | /api/files/:fileId | Yes | No | Download latest version (shared-link readable) |
PUT | /api/files/:fileId | Yes | Yes | Save a new version |
PATCH | /api/files/:fileId | Yes | Yes | Rename a file |
DELETE | /api/files/:fileId | Yes | Yes | Delete file and all versions |
GET | /api/files/:fileId/versions | Yes | Yes | List all versions of a file |
GET | /api/files/:fileId/versions/:versionId | Yes | Yes | Download a specific version |
Architecture Notes
- External file storage: File bytes are stored on a separate file storage service (addressed via
FILE_SERVER_URL), authenticated with anX-Internal-Keyheader (INTERNAL_SECRET). The database only stores metadata and storage paths. - Version retention: A maximum of 4 versions per file is enforced. When a new version is saved via
PUT, the oldest versions beyond the limit are pruned automatically. - Transaction safety: All write operations use PostgreSQL transactions. Storage byte deletion happens after commit on a best-effort basis, so a storage failure never leaves the database in an inconsistent state.
- Shared reads:
GET /api/files/:fileIdallows any authenticated user to read a file (enabling shared links), but all write operations verify file ownership againstuser_id.
File Storage Server
The application delegates raw byte storage to a separate Express server (the file storage server). This server is not user-facing -- it is called internally by the main application API when uploading, downloading, or deleting file bytes. It runs on port 4000 and stores files on disk under a configurable root directory.
Authentication
All endpoints (except /health) require the X-Internal-Key header matching the INTERNAL_SECRET environment variable. Requests without the correct key receive 403 Forbidden. This ensures only the main application backend can interact with the storage server.
Path Traversal Protection
Every route validates its userId and filename path parameters through a safeSegment() check that rejects values containing .., /, or \. This prevents directory traversal attacks that could read or write outside the storage root.
Endpoints
GET /internal/:userId/:filename
Download a stored file as a raw byte stream.
Response 200 OK -- File bytes streamed directly to the client.
Returns 400 Bad Request if path parameters fail validation, or 404 Not Found if the file does not exist on disk.
POST /internal/:userId/:filename
Upload a file. The request body is piped directly to disk as a raw byte stream (no multipart parsing).
Request:
- Body: raw binary stream (piped via
req.pipe(writeStream))
Server-side flow:
- Creates the user directory (
STORAGE_ROOT/:userId/) if it does not exist - Pipes the request body directly to
STORAGE_ROOT/:userId/:filename - Returns
201 Createdon successful write
Returns 400 Bad Request if path parameters fail validation, or 500 Internal Server Error if the write fails.
DELETE /internal/:userId/:filename
Delete a stored file from disk.
Response 200 OK -- "Deleted" on success.
Returns 400 Bad Request if path parameters fail validation, or 404 Not Found if the file does not exist.
GET /health
Health check endpoint. No authentication required.
Response 200 OK -- "OK"
Configuration
| Variable | Description |
|---|---|
INTERNAL_SECRET | Shared secret for X-Internal-Key authentication (required) |
STORAGE_ROOT | Filesystem root for stored files (default: /var/lib/app-storage) |
PORT | Server listen port (default: 4000) |
Storage Layout
/var/lib/app-storage/
├── <userId>/
│ ├── <uuid>.json -- version 1 of a file
│ ├── <uuid>.json -- version 2 of a file
│ ── ...
├── <userId>/
│ └── ...
Files are stored under per-user directories named by UUID. The main application generates UUID-based filenames with the original file extension preserved, so the storage server never sees user-provided filenames on disk.
Endpoint Summary
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET | /internal/:userId/:filename | Internal key | Download a stored file |
POST | /internal/:userId/:filename | Internal key | Upload a file (raw body stream) |
DELETE | /internal/:userId/:filename | Internal key | Delete a stored file |
GET | /health | None | Health check |