AppThrust developer CLIskill: appthrust-app-dev

Ship an app to appthrust.dev from your coding agent.

apth creates the GitHub repository, builds the image, provisions a managed PostgreSQL, and publishes a public URL. The bundled skill teaches Claude Code, Codex, and OMP the whole loop, so you only describe the app.

  1. 1

    Get apth

    Build the developer CLI from appthrust/platform.

    go build -o ~/.local/bin/apth ./apth-cli/cmd/apth
  2. 2

    Install the skill

    Writes appthrust-app-dev into Claude Code, Codex, and OMP skill directories.

    apth skills install
  3. 3

    Log in and ask your agent

    Device login in the browser, then a plain request does the rest.

    apth login --api-url https://dashboard.appthrust.dev
    # "make a small game and put it on appthrust.dev"

This is the text your agent reads after apth skills install. It is the operating manual for one application: repository, image, stack, database, publish.

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.

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)

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

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)

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

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

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

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:

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:

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.

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

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

Troubleshooting

SymptomMeaning / action
apps wait pending: component web ... SecretNotFound ... -web-envconnection env not projected yet; normal for ~1 min after create
component web ... ImagePullBackOff or image tag never resolvesGHCR package private or Actions build failed: gh run list --repo appthrust/<name>, package visibility
publish pending: DNS not readycertificate/DNS propagation, usually < 3 min; check apth publishes readiness
HTTP 403 actor gateway rejected credentialuse a client-credentials login for actor calls
project is requiredapth context set --project <pj> or --project
placement zone is ambiguouspass --placement <zone-id> from apth placement-zones list
GitHub repository ... already existsa 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: falsecredential 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 pushit saw the previous release; rerun with --released-after/--release-version
apps wait stays on release: waiting for a release created after ... for > 5 minthe 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.