# Files and background tasks

## Where files belong

Store end-user uploads and generated documents in Launchbase Files: profile photos, attachments, PDFs, exports and similar content. Never use the app container filesystem as permanent storage for these files; redeployments and replacements can lose them. Bundled assets such as the app logo may ship with the source. Temporary processing files may use the filesystem and must be cleaned up; upload any result that must survive to Files.

Store file IDs, names, owner IDs and other metadata in the app database. Keep the binary content in Files. Implement these operations in the app backend after authenticating the user:

| Operation | Implementation |
| --- | --- |
| Create | Reserve/upload/finalize with Launchbase Files, then save the file ID and owner in the database. |
| Read and list | Query the user's authorised database records; request a private download URL only after checking ownership. Runtime access has no end-user listing endpoint. |
| Update | Upload and finalize a new immutable file; switch the database reference; delete the old file. Keep the old reference if the replacement fails. Rename/display metadata belongs in the database. |
| Delete | Check ownership, delete via Launchbase Files, then mark/remove the database record. Retry safely if either step fails. |

Never put a management token or LB_RUNTIME_TOKEN in browser code. Test two different users: neither can list, download, replace or delete the other's private files, including by guessing a file ID. Set `Cache-Control: private, no-store` on file-related backend responses. App-level isolation does not replace these end-user checks.

Files have their own allowance: 1 GB during a trial, 5 GB on Launch, 25 GB on Grow and 100 GB on Scale. The allowance measures current stored bytes, not monthly uploads. Upgrade the whole app for more space. At the limit, new uploads stop; existing files remain available. Deleting files frees space after outstanding upload links expire (up to about two hours, including hourly cleanup).

Background workers and scheduled tasks use the app's existing Hosting allowance. They stop when the app is paused, hosting is disabled, payment is blocked or Hosting reaches its enforced limit. Trial deadlines never restart. Files survive deployments and database pause/restore. Paused trial files follow the existing retention deadline: 30 days after the original trial end, then managed resources are deleted.

## Connect file storage

1. Select the app and wait until hosting is provisioned.
2. MCP: `configure_file_storage {appId, enabled:true}`. REST: `PUT /api/v1/apps/{appId}/storage` with `{"enabled":true}`.
3. MCP: `configure_app_runtime {appId}`. REST: `POST /api/v1/apps/{appId}/runtime`. This injects `LB_RUNTIME_URL` and `LB_RUNTIME_TOKEN` into the app's server environment. Deploy again to activate them.
4. Add authenticated backend routes for uploading and downloading files. **Never expose the runtime token to a browser.** It authorizes the app's files, not individual end users. Your backend must authenticate the user, check ownership, and store each returned file ID with its owner in the app database. Apply database row-level security to that ownership table. Do not accept arbitrary file IDs without checking ownership.

### Upload from your backend

The runtime API uses `Authorization: Bearer $LB_RUNTIME_TOKEN`. Unlike the management API, runtime responses are plain JSON, without a `data` wrapper.

```js
import {createHash, randomUUID} from 'node:crypto';
// Authenticate your app user and enforce their permissions before this code.
const bytes = Buffer.from('Example document');
const headers = {
  Authorization: `Bearer ${process.env.LB_RUNTIME_TOKEN}`,
  'Content-Type': 'application/json'
};
const base = process.env.LB_RUNTIME_URL;
const response = await fetch(`${base}/files`, {
  method: 'POST', headers,
  body: JSON.stringify({
    name: 'example.txt', contentType: 'text/plain', sizeBytes: bytes.length,
    sha256: createHash('sha256').update(bytes).digest('hex'), requestId: randomUUID()
  })
});
if (!response.ok) throw new Error(await response.text());
const intent = await response.json();
if (intent.upload) {
  const uploaded = await fetch(intent.upload.url, {
    method: 'PUT', headers: intent.upload.headers, body: bytes
  });
  if (!uploaded.ok && uploaded.status !== 412) throw new Error(`Upload failed: ${uploaded.status}`);
  // 412 means the same immutable file was already uploaded; finalize verifies it.
  const finalized = await fetch(`${base}/files/${intent.file.id}/finalize`, {
    method: 'POST', headers, body: '{}'
  });
  if (!finalized.ok) throw new Error(await finalized.text());
}
// Save intent.file.id alongside the authenticated user's ID in your database.
```

Maximum file size: 50 MB. Upload links expire after ten minutes. The declared size and SHA-256 are enforced by storage; files cannot be overwritten. Use a new ID for a replacement and delete the old file afterwards. Retry an upload intent with the same requestId and identical metadata. Do not send metadata or credentials to logs.

After checking user ownership, your backend can call:

- `GET /runtime/v1/files/{fileId}/download`: five-minute private download URL. Redirect the authorized user to it.
- `DELETE /runtime/v1/files/{fileId}`: hide the file and schedule physical deletion.

Files are private downloads, not a public website hosting bucket. Scan untrusted uploads in your own app before using them. To list and export files, management clients use `GET /api/v1/apps/{appId}/files`; follow `nextAfter` using `?after=...`, then request each file's `/download` link. Download before the five-minute link expires and verify the bytes. The Files screen offers individual downloads too.

### Export all files

Download `/files-export.mjs`. Set `LAUNCHBASE_TOKEN` and `LAUNCHBASE_APP` in your local environment, then run `node files-export.mjs ./my-files` with a new destination directory. The script downloads all pages and verifies sizes and checksums. It stores files under their IDs and preserves original names in `manifest.json`. Run it before app retirement. The normal app export also includes this script when uploaded files exist.

## Background workers

Upload a source version containing your web app and worker scripts. Use the verified sourceId:

```json
{
  "name": "process-documents",
  "sourceId": "YOUR_VERIFIED_SOURCE_ID",
  "startCommand": "python worker.py",
  "requestId": "worker-release-001"
}
```

Send it to `POST /api/v1/apps/{appId}/tasks`, or use MCP `deploy_task` with appId. Omit schedule for a continuously running worker. Use `node worker.mjs` for Node.js. Dockerfile apps are supported too; the task command overrides CMD.

Poll the returned release with `get_release` or `GET /api/v1/apps/{appId}/releases/{releaseId}`. Read both build and runtime logs on failure. A running process does not prove that your business logic succeeded: test it with a real job.

Workers receive the app's runtime token and unprivileged Supabase configuration. Database security still applies. Do not use a provider master key to bypass it; design narrow database permissions or authenticated service users for the work your task needs.

## Scheduled tasks

Add `"schedule":"0 9 * * *"` to run at 09:00 UTC each day. Five-field UTC cron is supported with numeric fields or `*`; the minute field accepts a fixed minute or intervals `*/5`, `*/6`, `*/10`, `*/12`, `*/15`, `*/20`, `*/30`. More complex expressions are rejected. There are up to five background services per app.

Scheduled code must finish and exit. A run may occur during deployment or manual resume, so make the operation safe to repeat. Railway skips the next scheduled run if the previous one has not finished. Scheduling is approximate, not precise to the second. There is no automatic catch-up after an app is paused.

For durable jobs, store pending work in the database, claim it atomically with a lease, mark completion, and retry failures with a capped attempt count. Make effects idempotent using a stable job ID. These job semantics belong in your app; deploying a worker does not add a queue automatically.

`list_tasks`, `pause_task`, and `resume_task` are available in MCP. REST equivalents are `/tasks`, `/tasks/{serviceId}/pause`, and `/tasks/{serviceId}/resume`. Owners can pause/resume from Services → Tasks. A manually paused task stays paused when the rest of the app resumes.

## Provider cost model

Railway bucket storage is separate from CPU and memory. Its published price is $0.015 per GB-month; bucket requests and direct download egress are free. Traffic uploaded by an app server counts as that server's egress and therefore Hosting usage. Retained files still incur storage cost while compute is paused. Source: https://docs.railway.com/storage-buckets/billing (checked 15 September 2026).
