Skip to main content
Version: Sprint 2

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:

  1. Uploads file bytes to the external file storage service under a UUID-named path
  2. Inserts a row into user_files
  3. Inserts a row into file_versions with version_number = 1
  4. 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:

HeaderDescription
Content-TypeMIME type of the stored file
Content-Dispositionattachment; filename="<original-name>"
X-Version-NumberVersion 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:

  1. Uploads new file bytes to storage under a fresh UUID path (old version bytes remain untouched)
  2. Computes nextVersion = MAX(version_number) + 1 for the file
  3. Inserts the new version row
  4. Updates user_files.updated_at
  5. Prunes versions beyond the retention limit (keeps the latest 4) — deletes DB rows first, then best-effort deletes storage bytes after commit
  6. 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:

  1. Fetches all version storage_path values for the file
  2. Deletes each version's bytes from the external file storage service
  3. Deletes the user_files row (cascades to file_versions via 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:

HeaderDescription
Content-TypeMIME type of the stored file
Content-Dispositionattachment; filename="<original-name>"
X-Version-NumberVersion 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​

MethodEndpointAuthOwner-onlyDescription
GET/api/filesYesYesList user's files with latest version info
POST/api/filesYesYesCreate a new file (version 1)
GET/api/files/:fileIdYesNoDownload latest version (shared-link readable)
PUT/api/files/:fileIdYesYesSave a new version
PATCH/api/files/:fileIdYesYesRename a file
DELETE/api/files/:fileIdYesYesDelete file and all versions
GET/api/files/:fileId/versionsYesYesList all versions of a file
GET/api/files/:fileId/versions/:versionIdYesYesDownload 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 an X-Internal-Key header (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/:fileId allows any authenticated user to read a file (enabling shared links), but all write operations verify file ownership against user_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:

  1. Creates the user directory (STORAGE_ROOT/:userId/) if it does not exist
  2. Pipes the request body directly to STORAGE_ROOT/:userId/:filename
  3. Returns 201 Created on 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​

VariableDescription
INTERNAL_SECRETShared secret for X-Internal-Key authentication (required)
STORAGE_ROOTFilesystem root for stored files (default: /var/lib/app-storage)
PORTServer 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​

MethodEndpointAuthDescription
GET/internal/:userId/:filenameInternal keyDownload a stored file
POST/internal/:userId/:filenameInternal keyUpload a file (raw body stream)
DELETE/internal/:userId/:filenameInternal keyDelete a stored file
GET/healthNoneHealth check