# Bring Your Own Agent

Speed apps are normally built by the Speed agent inside the dashboard. You can
also work on one from your own machine, with your own editor or your own coding
agent, and get the result onto the platform through the HTTP API: pull the
source, run it locally, upload a new version, preview it, deploy it, and read
what happened.

This page covers that workflow. The app itself — what it is made of and the
conventions it follows — is covered in
[Speed App Development](./Speed-App-Development.md). Both pages are written to be
handed to a coding agent as they are; the raw Markdown is available at the same
URL with a `.md` extension.

There is no UI for any of this. The endpoints are the ones the dashboard itself
uses, so they are stable in practice, but they are not versioned and may change;
check back here when something stops working.

## Getting started

1. Create a personal access token (below) and give it to your agent together
   with a link to this page.
2. Download the app's source, or create an empty app, as described in the next
   sections.
3. Run the app locally in the Speed startkit container, and let the agent read
   the conventions shipped inside it (see [Speed App Development](./Speed-App-Development.md)).
4. Upload, preview and deploy through the API as described further down.

## Authentication

Every request below carries a personal access token as a bearer token. Create
one in the dashboard under Settings → Access Tokens; it starts with `spd_`, is
shown once, and works until you delete it. The token acts as you, with your role
on each app, so keep it out of the source tree and out of the tarball.

```bash
export SPEED_TOKEN=spd_...
```

App ids and checkpoint ids are numeric; `:appId` also accepts the app's name.

## Starting from scratch

An app does not have to begin with the Speed agent. `POST /api/apps` with
`{"name": "my-app", "empty": true}` creates an app whose only checkpoint is the
untouched project template, and runs no agent. The name becomes the app's
`<name>.spdso.app` domain, so it must be lowercase letters, digits and hyphens.

```bash
curl -X POST https://api.speed.so/api/apps \
  -H "Authorization: Bearer $SPEED_TOKEN" -H "Content-Type: application/json" \
  -d '{"name": "my-app", "empty": true}'
```

The response is the app plus a `checkpointId`. Download that checkpoint as
described next to get the template, run it locally, develop it following
[Speed App Development](./Speed-App-Development.md), then upload and deploy.

The same template is also inside the startkit image at `/tmp/repo`, if you want
to look at it before creating an app:

```bash
docker create --platform linux/amd64 --name speed-template ghcr.io/xinxiaotech/speed-startkit:latest
docker cp speed-template:/tmp/repo/. ./my-app && docker rm speed-template
rm -rf my-app/node_modules my-app/dist
```

## Getting the source

`POST /api/apps/:appId/checkpoints/:checkpointId/download` returns the
checkpoint's source as a tar.gz. Download the app's production checkpoint
(`productionCheckpointId` from `GET /api/apps/:appId`) to start working on an app
the agent built, or to pick up changes the agent made since your last upload.

```bash
curl -X POST "https://api.speed.so/api/apps/$APP_ID/checkpoints/$CHECKPOINT_ID/download" \
  -H "Authorization: Bearer $SPEED_TOKEN" -o /tmp/source.tar.gz
```

## Running locally

Run your app in the same Docker image the platform uses, against the production
platform services. Requirements: Docker, and your app's source tree.

The image is public and built for `linux/amd64`, which is what production runs.
On an Apple Silicon machine Docker runs it under emulation; it works, but the
first `pnpm install` is slower.

```bash
docker pull ghcr.io/xinxiaotech/speed-startkit:latest
```

From the app directory, fetch the Speed SDK assets that the edge proxy serves in
production (they are gitignored, and only needed locally):

```bash
mkdir -p public/_speed
curl -fsSL https://dash.speed.so/speed-sdk.js            -o public/speed-sdk.js
curl -fsSL https://dash.speed.so/speed-service-worker.js -o public/speed-service-worker.js
curl -fsSL https://dash.speed.so/inject-entry.js         -o public/_speed/inject-entry.js
```

Then start the app. Keep the container name `speed-dev`: the conventions shipped
in the image (see [Speed App Development](./Speed-App-Development.md#conventions-for-the-agent))
refer to it. Add `-it` if you are running it from an interactive terminal.

```bash
docker run --rm --name speed-dev -v "$PWD:/tmp/repo" -p 3000:3000 -p 6006:6006 \
  -e NODE_ENV=development -e SPEED_APP_ID=123 -e SPEED_APP_KEY=... \
  ghcr.io/xinxiaotech/speed-startkit:latest \
  sh -c 'pnpm install && node --import tsx /tmp/framework/server.ts'
```

The app comes up on http://localhost:3000 and Storybook on http://localhost:6006,
with frontend HMR and API hot reload. The database lives in `./.data`, so it
survives restarts and stays out of production.

The database starts empty. On the first start, once `pnpm install` has finished,
apply the schema; the platform does this for you on upload and deploy, but
locally nothing does:

```bash
docker exec speed-dev pnpm run db:apply
```

`SPEED_APP_ID` (from the app's dashboard URL) and `SPEED_APP_KEY` (from
`GET /api/apps/:appId`, see [Other endpoints](#other-endpoints)) are only needed for server-side LLM, image, analytics and upload calls. The
platform endpoints default to production, so nothing else has to be configured.

The bind mount hides the image's `node_modules`, which is why `pnpm install` runs
on start. It installs into your app directory, so only the first run is slow —
though that first run does compile `better-sqlite3`, and takes longer still if
the image's architecture does not match your machine.

## Signing in

The normal login flow does not work from localhost: the platform resolves the app
from the domain of the redirect URL, and `localhost` is not one of your app's
domains. Borrow a real token from the deployed app instead:

1. Open `https://<your-app>.spdso.app` and sign in.
2. In the browser console, run:
   ```js
   copy(JSON.stringify(await speed.auth.getCurrentUser()))
   ```
3. Open `http://localhost:3000/?_speed_auth_data=<the JSON, URL-encoded>`.

The SDK picks the token up, stores it and strips it from the URL, so
`speed.auth.getCurrentUser()` works from then on. Its service worker registers on
localhost as it does in production, so your app's own API calls carry the token
without doing anything else. The token expires after a week; repeat the steps to
refresh it.

A real token is what the platform's own endpoints need. Your app's backend is
less strict: in development the framework decodes the token without checking its
signature, so if you only need `ctx.var.userId` and `ctx.var.userRole`, any
decodable JWT carrying `appId`, `userId` and `role` will do.

## What differs from production

- There is no edge proxy in front of the app, so what it does — access control
  for private apps, analytics collection, serving the SDK — either does not
  happen or is stood in for by the files you fetched into `public/`.
- You are running the dev server, not the production build. Run `pnpm run build`
  before uploading if you want to be sure the build passes.
- `node_modules` is installed for the image's platform (Linux), and
  `speed-framework` resolves to the framework shipped in the image. Your editor
  will not resolve those imports; run `pnpm run check` inside the container.
- `.data` here is your own throwaway database. Uploading source never touches
  production data.
## Uploading a new version

`POST /api/apps/:appId/upload-source` turns a tarball of the source into a
checkpoint, which can then be previewed and deployed. Nothing runs on your side:
no workspace, no agent session, no Docker.

### Packing the tarball

Pack from *inside* the project directory so the entries have no leading directory
component, and leave out what the platform installs or builds for you:

```bash
COPYFILE_DISABLE=1 tar --no-xattrs --no-mac-metadata \
  --exclude=./node_modules --exclude=./.pnpm-store --exclude=./.git --exclude=./.data --exclude=./dist \
  --exclude=./public/speed-sdk.js --exclude=./public/speed-service-worker.js --exclude=./public/_speed \
  -czf /tmp/source.tar.gz -C /path/to/app .
```

The tarball is the whole source tree, not a patch: a file you leave out is a file
you delete.

The xattr flags are only needed on macOS, where tar otherwise records
`com.apple.provenance` on every file and the upload fails with a `500`.

### Uploading

```bash
curl -X POST "https://api.speed.so/api/apps/$APP_ID/upload-source?title=My%20change" \
  -H "Authorization: Bearer $SPEED_TOKEN" \
  -H "Content-Type: application/gzip" \
  --data-binary @/tmp/source.tar.gz
```

Requires the `contributor` role on the app. On success:

```json
{
  "sessionId": 123,
  "checkpointId": 456,
  "imageName": "...",
  "published": true,
  "setupLogs": "..."
}
```

`setupLogs` is the output of installing dependencies and applying the schema. If
that step fails — a broken `package.json`, a schema `db:apply` rejects — the
response is `422` with the same `setupLogs` and no checkpoint is created; fix the
source and upload again. A `published: false` means the checkpoint was created but
its image could not be stored, so deploying it will fail: upload again.

The request runs the install and schema steps, so it can take minutes, and an edge
timeout may cut the response short. The upload itself keeps going — look for the
resulting checkpoint with `GET /api/apps/:appId/checkpoints` instead of retrying
blindly.

Each upload creates its own session holding a single checkpoint, so an app updated
this way accumulates one session per upload.

## Previewing

`POST /api/apps/:appId/checkpoints/:checkpointId/preview` starts the checkpoint
at `https://checkpoint-<id>.dev.spdso.app`, on a copy of the session data.
`?copyDataFrom=production` copies production data instead.

## Deploying

Deploy the checkpoint with the normal endpoint:

```bash
curl -X PUT "https://api.speed.so/api/apps/$APP_ID/deploy?checkpointId=$CHECKPOINT_ID" \
  -H "Authorization: Bearer $SPEED_TOKEN"
```

The deploy runs in the background; the response carries a `deployId`, and
`GET /api/apps/:appId/deploy/:deployId` reports `status` (`pending`, `running`,
`completed`, `failed`) and `error`. `GET /api/apps/:appId/deployments` lists
past deployments.

Two things happen at deploy time rather than at upload time: the production build
(`pnpm run build`), which makes the first deploy of an uploaded checkpoint slower
than usual, and the migration of the production database from `db/schema.ts`. The
schema step during upload only touches a throwaway database inside the image, so
production data is never affected by an upload alone.
`POST /api/apps/:appId/deploy/dry-run?checkpointId=` reports what the migration
would do, without doing it.

## Querying the production database

`POST /api/apps/:appId/data-viewer` (requires `contributor`) starts a data
viewer for the app's production database and returns its `url`, the `database`
path, and an `adminerPassword`:

```bash
curl -X POST "https://api.speed.so/api/apps/$APP_ID/data-viewer" \
  -H "Authorization: Bearer $SPEED_TOKEN"
```

The viewer's `sql-runner.php` then runs any SQL statement. It is authenticated by
the `adminerPassword` from the response, not by your access token:

```bash
curl -X POST "$URL/sql-runner.php?db=$DATABASE" \
  -H "Authorization: Bearer $ADMINER_PASSWORD" -H "Content-Type: application/json" \
  -d '{"sql": "SELECT name FROM sqlite_master WHERE type = '"'"'table'"'"'"}'
```

The response is `{"ok": true, "columns": [...], "rows": [[...]], "rowCount": n}`,
or `{"ok": false, "error": "..."}`.

**This runs directly against the production database, writes and DDL included,
with no confirmation and no undo.** Read before you write, and never let an agent
run statements here that it has not shown you first. The same endpoint exists per
session at `POST /api/apps/:appId/sessions/:sessionId/data-viewer` for that
session's data.

## Getting an app token

Your app's own API accepts platform JWTs, not personal access tokens. To call it
with curl or from a script, exchange your access token for one:

```bash
curl -si -H "Authorization: Bearer $SPEED_TOKEN" \
  "https://api.speed.so/platform/auth?redirect=http://localhost:3000/&appId=$APP_ID" \
  | grep -i '^location:'
```

The `Location` header carries `_speed_auth_data`, a URL-encoded JSON whose
`token` is a one-week platform JWT for the app. Use it as the bearer token
against `https://<your-app>.spdso.app` or your local app, or open the URL in a
browser to sign the local app in directly (see [Signing in](#signing-in)).

## Other endpoints

- `GET /api/apps/:appId` — the app, including `productionCheckpointId`, the
  `appKey` needed to run the app locally, and `environments`.
- `GET /api/apps/:appId/dev-logs?follow=true` — the production container's
  stdout/stderr as an SSE stream (last 100 lines, then live).
- `PUT /api/apps/:appId` with `{"environments": {...}}` — sets the app's
  environment variables. The object replaces the whole set, so read it from
  `GET /api/apps/:appId` and send it back with your changes. Each entry is
  `{"value": "...", "description": "...", "prodOnly": false}`.

## iOS apps

Apps that ship to the App Store have two more pages: [Sign in with Apple](./Apple-Sign-In.md)
and [Apple In-App Purchases](./Apple-IAP.md).
