---
name: appthrust-app-dev
description: Build and ship a web app (Next.js + managed PostgreSQL, optionally AppThrust Actors) to appthrust.dev from a local Claude Code or Codex session using the apth CLI. Use when asked to create, deploy, update, publish, configure, or debug an application on AppThrust, e.g. "make a small game and put it on appthrust.dev", "deploy this repo to AppThrust", "add an env var to my AppThrust app", "why is my AppThrust app not ready", or Japanese requests such as アプリを作ってappthrust.devに置いて, AppThrustにデプロイして.
---

# AppThrust App Dev

One AppThrust *application* = a GitHub repository + a container image + an
`ApplicationStack` (web component + PostgreSQL + connection) + an optional
public URL. Everything below goes through `apth` (developer CLI) and `gh`.
Never call the Platform API with curl when an `apth` command exists.

```text
edit code → git push main → GitHub Actions builds ghcr.io/<org>/<repo>:edge-<ts>
          → AppThrust observes the newest edge- tag → releases it (~1–3 min after push)
          → https://<name>.appthrust.dev/
```

Measured on appthrust.dev (2026-09-16, `nextjs-postgres` template): `apps
create` → stack Ready + public URL serving in 3.5 min; a later `git push` →
image built → released in about 1 min. The whole "template → own game →
verified on the public URL" loop fits in 5–6 minutes when the steps below are
overlapped; do not wait for one step when the next does not depend on it.

## Prerequisites (check, do not assume)

```bash
apth --help >/dev/null          # CLI present (build: go build -o ~/.local/bin/apth ./apth-cli/cmd/apth in appthrust/platform)
gh auth status                  # GitHub CLI logged in with push access to the org (appthrust)
node --version                  # 24.x matches the template Dockerfile
apth whoami                     # authenticated + project set; see Login
apth skills install             # refresh this skill after upgrading apth (add --force when it reports a diff)
```

Default environment for AppThrust development: API `https://dashboard.appthrust.dev`,
project `pj-af59d1dc379b` (name `sample`), placement zone `free-apne1-shared-2`,
publish domain `appthrust.dev`. Confirm with `apth projects list` and
`apth placement-zones list` when unsure; never guess a different environment.

## 1. Login

```bash
apth login --api-url https://dashboard.appthrust.dev --project pj-af59d1dc379b
```

`login` without `--token` starts a device login: it prints a URL such as
`https://dashboard.appthrust.dev/cli-login?user_code=XXXX` and waits. Show that
URL and code to the human, ask them to approve it in a browser, and keep the
command running (default wait 10 minutes). The token is stored in
`~/.config/appthrust/credentials.json` for the profile and is **never
printed**. `apth whoami` asks the API whether the stored token is still
accepted: `tokenValid: true` plus `login` means go; `authenticated: false`
with a `hint` means run `apth login` again (Dashboard CLI tokens last about
12 hours). Any command failing with `HTTP 401` means the same thing.

Non-interactive alternatives:

- `APTH_TOKEN=<token> apth ...` or `apth login --token <token>` when a token
  was issued elsewhere.
- Service account: `printf '%s\n' "$SECRET" | apth login --issuer https://iam.appthrust.dev/realms/appthrust --client-id <client> --client-secret-stdin --profile svc`.
  This is the only credential the **actor gateway** accepts (see Actors).

Persist defaults so later commands are short: `apth context set --project <pj>`.

## 2. Create the application

Pick a name that is a DNS label (`[a-z0-9-]`, ≤ 63 chars); it becomes the
GitHub repo, the image path, and the subdomain.

### A. Template path (fastest; AppThrust creates the repo)

```bash
apth apps create <name> --template nextjs-postgres --visibility public --publish --wait --timeout 15m
```

What happens: AppThrust creates `github.com/appthrust/<name>` from
`appthrust/template-nextjs` (Next.js 16 App Router + `pg` + Dockerfile +
`.github/workflows/deploy.yml`), GitHub Actions builds the first
`edge-<timestamp>` image, the stack (web + PostgreSQL + `database` connection)
is placed on the shared runtime, and a publish on `https://<name>.appthrust.dev/`
is requested. `--wait` blocks until every component is Ready and the URL is
serving; on timeout it prints exactly what is still pending.

**Overlap the provisioning with your own code.** The stack, database, DNS and
certificate take ~3.5 min and do not depend on your source. Run the create in
the background (or omit `--wait`), then as soon as the repository exists
(`gh repo view appthrust/<name>`, ~30 s) clone it and implement; your first
push replaces the template release. Waiting for the template's first release
before writing code serializes two independent steps.

Other templates: `apth application-templates list <pj>` (Go, Rust, Python,
Rails, Hono… all `<framework>-postgres`). `--port` overrides the web port.

If a create fails midway (for example a GitHub name collision), rerunning the
same command reuses the already-created source repository instead of failing.

### B. Bring-your-own repository or image

```bash
gh repo create appthrust/<name> --template appthrust/template-nextjs --public --clone
# ...edit, commit, push main; wait for the "Docker Build and Push" run:
gh run watch --repo appthrust/<name> --exit-status
apth apps create <name> --image ghcr.io/appthrust/<name>:^edge- --publish --wait
```

`^edge-` means "follow the newest tag with this prefix" (auto-release on
every push). A fixed tag (`:v1.2.0`) pins the release. The GHCR package must be
pullable by AppThrust: public packages work without credentials; otherwise
create a registry connection first (`apth registry-connections`).

## 3. Develop loop

```bash
gh repo clone appthrust/<name> && cd <name>
npm ci
# implement...
npm run lint && npm run build
git commit -am "feat: ..." && git push origin main
apth apps wait <name> --released-after "$(git log -1 --format=%cI)" --timeout 10m
apth apps get <name>                                      # Release: <tag> (Succeeded at ...), public URL
```

`apps wait --released-after` already covers the image build: it reports
`release: waiting for a release created after <time>` until GitHub Actions has
pushed the new tag and AppThrust has rolled it out. Do not chain
`gh run watch` in front of it — the Actions run keeps running (commit comment,
cleanup) after the image is pushed, and waiting for it adds minutes to every
iteration. Use `gh run list`/`gh run view --log-failed` only when `apps wait`
times out or the release never appears.

`apps wait` alone returns as soon as the *current* release is healthy; after a
push always pass `--released-after <commit time>` (or `--release-version
edge-<ts>`) so it waits for the image built from that commit. `Image:` in
`apps get` is the stack's initial image; `Release:` is what is rolled out now.

Rules that keep the deploy path working:

- Keep the template `Dockerfile`, `deploy.yml`, `output: "standalone"` in
  `next.config.ts`, and npm + `package-lock.json`. The image must listen on
  `0.0.0.0:$PORT` (3000).
- The build has **no network and no database**. Never require `DATABASE_URL`
  at build time; do not prerender pages that query the DB unless they handle a
  missing URL.
- Read the repo's `AGENTS.md`/`CLAUDE.md`; the template pins a Next.js version
  whose APIs may differ from your training data — check
  `node_modules/next/dist/docs/` before writing routes or server code.
- Keep secrets out of the repo and out of `NEXT_PUBLIC_*`.
- Keep `pg` (and anything reading `process.env`) out of files imported by a
  `"use client"` component. Put shared types and constants in their own module
  (for example `lib/scores.ts`) and import `lib/db.ts` only from server
  components and `"use server"` actions; design that split before writing the
  first file rather than refactoring after the bundle pulls `pg` into the
  browser.
- `npm run dev` has no `DATABASE_URL`, so a local run proves only the non-DB
  logic (the app must render its "not connected" state). The database path is
  proven on the public URL after the release; do not spend time trying to
  reproduce the managed database locally.

## 4. Database

The stack injects a managed PostgreSQL connection into the web component as
generated env vars: `DATABASE_URL`, `DATABASE_HOST`, `DATABASE_PORT`,
`DATABASE_NAME`, `DATABASE_USER`, `DATABASE_PASSWORD`, `DATABASE_READ_AVAILABLE`
(`apth apps env get <name>` lists them as "generated by connection").

Schema options:

- Demo/booth apps: create tables lazily and idempotently on first query
  (`CREATE TABLE IF NOT EXISTS ...` behind a once-per-process promise).
- Product apps: declare a change with `apth databases changes create` (see
  `apth databases changes --help`); the template's initial schema
  (`appthrust_demo_messages`) is applied this way at create time.

Inspect data without a client: `apth databases sql <pj> <database> --help`,
`apth databases tables`, `apth databases rows`.

## 5. Configuration and logs

```bash
apth apps env get <name>
apth apps env set <name> LOG_LEVEL=debug FEATURE_X=on
apth apps env set <name> KEYCLOAK_CLIENT_SECRET="$SECRET" --sensitive KEYCLOAK_CLIENT_SECRET
apth apps env unset <name> FEATURE_X
apth apps logs <name> --tail 200
apth components restart-workload --help          # when a config change needs a restart
```

`env set` merges: variables not named are preserved (sensitive values are
never re-sent or displayed). A change rolls the web workload automatically.

## 6. Publish and verify

`--publish` on create is the normal path. For an existing app:

```bash
apth apps publish <name> --wait            # https://<name>.appthrust.dev/
apth apps publish <name> --subdomain play  # https://play.appthrust.dev/
```

Verify like a user, not from status alone:

```bash
curl -sS -o /dev/null -w '%{http_code}\n' https://<name>.appthrust.dev/
curl -sS https://<name>.appthrust.dev/api/<route> | head -c 400
```

For an interactive UI (game, form that writes to the database), drive the
public URL once in a real browser and check the persisted result with a second
plain request. Do it in one scripted pass: for elements that move or re-render
on every event (a target that jumps on each tap, a timer-driven screen),
coordinate clicks race the re-render and time out — dispatch the DOM events
from `page.evaluate` instead and read the score/state back from the DOM. A
round of trial-and-error clicking costs more than the deploy itself.

Dashboard: `https://dashboard.appthrust.dev/dashboard/projects/<pj>/applications/<name>/overview`.

## 7. Actors (optional)

AppThrust Actors are per-entity stateful workers (one per match, session,
player…). On appthrust.dev they are served by the platform's actor runtime;
the runtime image is fixed, so an app **uses** actor methods, it does not ship
actor code. Available method sets today: Connect Four (`join`, `drop`,
`state`, `reset`), Reversi (`reversi-join`, `reversi-place`, `reversi-state`,
`reversi-reset`), and a generic counter for any other method name (each call
increments and returns a per-actor counter — enough for "one actor per
player/session" demos). New game logic needs a platform change
(`pkg/actor/runtimehost` in appthrust/platform), not an app change.

```bash
apth actors types list                                   # e.g. connect-four
apth actors types create <name> --class Standard --storage KVSQL
apth actors types live connect-four                      # Active / Starting / Stopping / Sleeping
apth actors create connect-four match-1 --method join --body-json '{"player":"p1","name":"Reo"}'
apth actors invoke connect-four match-1 --method state
```

`actors create|invoke` require a Keycloak-issued JWT (client credentials login
above); a Dashboard device-login token is rejected by the actor gateway with
HTTP 403. The same applies inside the app: mint a `client_credentials` token
server-side (issuer `https://iam.appthrust.dev/realms/appthrust`) and call
`POST {PLATFORM_API_URL}/api/v1/projects/{pj}/actor-types/{type}/actors` with
`{actorId, method, body}` and an `Idempotency-Key`. Store `KEYCLOAK_CLIENT_ID`
/ `KEYCLOAK_CLIENT_SECRET` with `apps env set --sensitive`; the human provides
the client — the app must not create clients or widen permissions. Reference
implementation: `github.com/appthrust/tgs-connect-four` (`lib/appthrust.ts`).
Live actor visualisation: `https://dashboard.appthrust.dev/dashboard/projects/<pj>/actors`.

## 8. Cleanup

```bash
apth apps delete <name> --yes          # stack, components, database, publish
gh repo delete appthrust/<name> --yes  # only when the human confirms
```

## Troubleshooting

| Symptom | Meaning / action |
| --- | --- |
| `apps wait` pending: `component web ... SecretNotFound ... -web-env` | connection env not projected yet; normal for ~1 min after create |
| `component web ... ImagePullBackOff` or image tag never resolves | GHCR package private or Actions build failed: `gh run list --repo appthrust/<name>`, package visibility |
| `publish pending: DNS not ready` | certificate/DNS propagation, usually < 3 min; check `apth publishes readiness` |
| `HTTP 403 actor gateway rejected credential` | use a client-credentials login for actor calls |
| `project is required` | `apth context set --project <pj>` or `--project` |
| `placement zone is ambiguous` | pass `--placement <zone-id>` from `apth placement-zones list` |
| `GitHub repository ... already exists` | a previous attempt created it; rerun the same `apps create` (source is reused) or pass `--source-repository <id>` |
| `HTTP 401` on any command, or `whoami` reports `tokenValid: false` | credential expired or revoked: `apth login` again (the CLI prints the exact `apth login --profile ...` hint) |
| `HTTP 409 conflict (delivery_pending)` | the platform's GitOps root is still syncing; the SDK already retried for ~30 s. Wait a minute and rerun the same command once |
| `HTTP 409 conflict (delivery_blocked)` | the platform's delivery root is unhealthy; not caused by your request and retrying will not help. Tell the human to ask a platform admin |
| `apps wait` returns immediately after a push | it saw the previous release; rerun with `--released-after`/`--release-version` |
| `apps wait` stays on `release: waiting for a release created after ...` for > 5 min | the image build did not produce a tag: `gh run list --repo appthrust/<name>` then `gh run view <id> --log-failed`; fix the source and push again |

## Safety

- Never print, log, or commit tokens, client secrets, or `DATABASE_URL` values.
- Mutations are idempotent-keyed by the SDK; `delete` needs `--yes`.
- Only touch the application you were asked about; other apps in the project
  belong to other demos.
