Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 103 additions & 55 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,53 @@ Public images from Docker Hub (e.g., `postgres:16-alpine`, `redis:7-alpine`, `ng

Upstream pre-built images from other registries (e.g., `ghcr.io/org/app:stable`) are also fine — see "Pre-built Upstream Images" below.

## Authentication (single token, HTTPS-only)

**You only need ONE credential: a N0 Personal Access Token (PAT).** Everything —
n0 API calls, workspace lookup, Gitea repo creation, git push, Supabase — is done
over **HTTPS** with this single token. This is the path that works inside the
Claude Desktop / Claude Code sandbox (outbound HTTPS only, no SSH, no interactive
web login, no server SSH).

The user provides the token via two environment variables (generated by the
"Build an app with Claude" button in N0 → Developer Settings → API Tokens):

```bash
export N0_API_BASE="https://app.nzero.pro/api/v1" # or the workspace's platform host
export N0_API_TOKEN="clovr_pat_..." # scopes: apps, gitea:write, workspaces:read, supabase
```

Use it as a Bearer token on every n0 API call:

```bash
curl -s -H "Authorization: Bearer $N0_API_TOKEN" "$N0_API_BASE/workspaces/"
```

**Getting the Gitea token (HTTPS, no web UI, no SSH):**
Mint a short-lived Gitea access token from the n0 API using the PAT. This is the
ONLY approved way to get Gitea credentials in the sandbox:

```bash
# Requires the PAT to include the gitea:write scope
GITEA=$(curl -s -X POST "$N0_API_BASE/workspaces/$WS_ID/gitea/token/" \
-H "Authorization: Bearer $N0_API_TOKEN")
GITEA_TOKEN=$(echo "$GITEA" | python3 -c "import json,sys; print(json.load(sys.stdin)['data']['token'])")
GITEA_USER=$(echo "$GITEA" | python3 -c "import json,sys; print(json.load(sys.stdin)['data']['username'])")
GITEA_URL=$(echo "$GITEA" | python3 -c "import json,sys; print(json.load(sys.stdin)['data']['gitea_url'])")
GITEA_HOST=$(echo "$GITEA_URL" | sed -e 's#^https\?://##' -e 's#/$##')
```

The response is `{"success": true, "data": {"token": "...", "username": "...", "gitea_url": "https://gitea-<slug>.apps.<domain>"}}`.
The Gitea token is scoped to `write:repository,write:package,write:organization`
and returned once (not stored) — use it for repo creation and git push, then discard.

**RULES (do not violate — SSH and interactive logins break in the sandbox):**
- ✅ Do all git operations over **HTTPS** with the Gitea token embedded in the remote URL:
`https://<username>:<gitea-token>@<gitea-host>/<org>/<repo>.git`
- ❌ **NEVER** use an SSH git remote (`git@...`, `ssh://...`) — no SSH keys exist in the sandbox.
- ❌ **NEVER** use email/password `/auth/login` — use the PAT Bearer token instead.
- ❌ **NEVER** create Gitea tokens via the web UI or via `gitea admin ... generate-access-token` over SSH — use the `gitea/token/` endpoint above.

## Workflow

Always follow this order:
Expand Down Expand Up @@ -1443,25 +1490,20 @@ After generating the manifest and pushing code to Gitea, the app must be **impor

### Prerequisites

1. **Workspace member JWT token** — any workspace member can import and deploy apps
1. **N0 Personal Access Token (PAT)** — supplied via `$N0_API_TOKEN` (see "Authentication" above). Any workspace member's PAT can import and deploy apps.
2. **Code + n0-app.json pushed to Gitea** — the repo must exist in the workspace's Gitea instance
3. **Gitea Actions build completed** — the Docker image must be in the registry before deploy

### Step 1: Import App Definition

Register the app in the workspace catalog by pointing at the Gitea repo:
Register the app in the workspace catalog by pointing at the Gitea repo. Use the
PAT as a Bearer token (no email/password login):

```bash
# Login to get JWT token
TOKEN=$(curl -s -X POST https://app.privateprompt.tech/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"you@example.com","password":"..."}' | \
python3 -c "import json,sys; print(json.load(sys.stdin)['data']['tokens']['access_token'])")

# Import app definition from Gitea repo
# NOTE: trailing slash is required on this endpoint
curl -s -X POST "https://app.privateprompt.tech/api/v1/workspaces/${WS_ID}/apps/definitions/" \
-H "Authorization: Bearer $TOKEN" \
curl -s -X POST "$N0_API_BASE/workspaces/${WS_ID}/apps/definitions/" \
-H "Authorization: Bearer $N0_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"repo_url": "clovrlabs/my-app"}'
```
Expand All @@ -1473,8 +1515,8 @@ This fetches `n0-app.json` from the repo, validates it, and creates an `AppDefin
### Step 2: Deploy App Instance

```bash
curl -s -X POST "https://app.privateprompt.tech/api/v1/workspaces/${WS_ID}/apps/" \
-H "Authorization: Bearer $TOKEN" \
curl -s -X POST "$N0_API_BASE/workspaces/${WS_ID}/apps/" \
-H "Authorization: Bearer $N0_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"app_type": "my-app-slug", "subdomain": "my-app"}'
```
Expand All @@ -1494,24 +1536,24 @@ The deploy is async — a background Huey task:
### Step 3: Redeploy (after code changes)

```bash
curl -s -X POST "https://app.privateprompt.tech/api/v1/workspaces/${WS_ID}/apps/${APP_ID}/redeploy" \
-H "Authorization: Bearer $TOKEN"
curl -s -X POST "$N0_API_BASE/workspaces/${WS_ID}/apps/${APP_ID}/redeploy" \
-H "Authorization: Bearer $N0_API_TOKEN"
```

### Other Operations

```bash
# Stop app
curl -X POST ".../apps/${APP_ID}/stop" -H "Authorization: Bearer $TOKEN"
curl -X POST "$N0_API_BASE/workspaces/${WS_ID}/apps/${APP_ID}/stop" -H "Authorization: Bearer $N0_API_TOKEN"

# Start app
curl -X POST ".../apps/${APP_ID}/start" -H "Authorization: Bearer $TOKEN"
curl -X POST "$N0_API_BASE/workspaces/${WS_ID}/apps/${APP_ID}/start" -H "Authorization: Bearer $N0_API_TOKEN"

# Delete/uninstall app
curl -X DELETE ".../apps/${APP_ID}" -H "Authorization: Bearer $TOKEN"
curl -X DELETE "$N0_API_BASE/workspaces/${WS_ID}/apps/${APP_ID}" -H "Authorization: Bearer $N0_API_TOKEN"

# Pre-deploy validation
curl ".../apps/${APP_ID}/validate" -H "Authorization: Bearer $TOKEN"
curl "$N0_API_BASE/workspaces/${WS_ID}/apps/${APP_ID}/validate" -H "Authorization: Bearer $N0_API_TOKEN"
```

### Image Mirroring (How K8s Pulls Images)
Expand Down Expand Up @@ -1540,6 +1582,11 @@ Both the loopback address (`127.0.0.1:{port}`) and external hostname (`gitea-{sl

### Troubleshooting Registry Credentials

> **Admin-only fallback — not part of the normal flow.** This requires server SSH
> and is NOT available in the Claude Desktop / Claude Code sandbox. Skip it unless
> you are a platform operator debugging stale org secrets. Normal app deploys never
> need this; use the `gitea/token/` endpoint (see "Authentication") for credentials.

If Gitea Actions builds succeed but `docker push` fails, the org-level Actions secrets may be stale:

```bash
Expand All @@ -1565,9 +1612,16 @@ Here's the full workflow for developing an app locally and deploying it to N0:

### 1. Create a Gitea Repo

First mint a Gitea token from the n0 API using your PAT (see "Authentication"
above — HTTPS-only, sandbox-safe). `GITEA_TOKEN`, `GITEA_USER`, and `GITEA_HOST`
come from `POST $N0_API_BASE/workspaces/$WS_ID/gitea/token/`:

```bash
GITEA_TOKEN="<your-gitea-personal-access-token>"
GITEA_HOST="gitea-clovrlabs.apps.privateprompt.tech"
GITEA=$(curl -s -X POST "$N0_API_BASE/workspaces/$WS_ID/gitea/token/" \
-H "Authorization: Bearer $N0_API_TOKEN")
GITEA_TOKEN=$(echo "$GITEA" | python3 -c "import json,sys; print(json.load(sys.stdin)['data']['token'])")
GITEA_USER=$(echo "$GITEA" | python3 -c "import json,sys; print(json.load(sys.stdin)['data']['username'])")
GITEA_HOST=$(echo "$GITEA" | python3 -c "import json,sys; print(json.load(sys.stdin)['data']['gitea_url'])" | sed -e 's#^https\?://##' -e 's#/$##')

curl -s -X POST "https://${GITEA_HOST}/api/v1/orgs/clovrlabs/repos" \
-H "Authorization: token $GITEA_TOKEN" \
Expand Down Expand Up @@ -1604,8 +1658,9 @@ Use this value in `n0-app.json` image fields. In the workflow YAML, always use `
### 4. Push to Gitea

```bash
# HTTPS remote with the Gitea token embedded — NEVER use an SSH remote (git@...)
git init && git add -A && git commit -m "Initial commit"
git remote add origin "https://<username>:${GITEA_TOKEN}@${GITEA_HOST}/clovrlabs/my-app.git"
git remote add origin "https://${GITEA_USER}:${GITEA_TOKEN}@${GITEA_HOST}/clovrlabs/my-app.git"
git push -u origin main --force
Comment on lines +1661 to 1664
```

Expand All @@ -1621,19 +1676,18 @@ curl -s -H "Authorization: token $GITEA_TOKEN" \
### 6. Import + Deploy via API

```bash
N0_TOKEN="<jwt-token>"
WS_ID="<workspace-uuid>"
API="https://app.privateprompt.tech/api/v1"
# N0_API_BASE and N0_API_TOKEN come from the environment (see "Authentication")
# WS_ID is the workspace UUID (look it up via GET $N0_API_BASE/workspaces/)

# Import app definition (trailing slash required)
curl -s -X POST "$API/workspaces/$WS_ID/apps/definitions/" \
-H "Authorization: Bearer $N0_TOKEN" \
curl -s -X POST "$N0_API_BASE/workspaces/$WS_ID/apps/definitions/" \
-H "Authorization: Bearer $N0_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"repo_url": "clovrlabs/my-app"}'

# Deploy (trailing slash required)
curl -s -X POST "$API/workspaces/$WS_ID/apps/" \
-H "Authorization: Bearer $N0_TOKEN" \
curl -s -X POST "$N0_API_BASE/workspaces/$WS_ID/apps/" \
-H "Authorization: Bearer $N0_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"app_type": "my-app", "subdomain": "my-app"}'
```
Expand All @@ -1642,57 +1696,51 @@ curl -s -X POST "$API/workspaces/$WS_ID/apps/" \

```bash
# Check app status
curl -s "$API/workspaces/$WS_ID/apps/" \
-H "Authorization: Bearer $N0_TOKEN" | \
curl -s "$N0_API_BASE/workspaces/$WS_ID/apps/" \
-H "Authorization: Bearer $N0_API_TOKEN" | \
python3 -c "import json,sys; apps=json.load(sys.stdin)['data']; [print(f'{a[\"subdomain\"]}: {a[\"status\"]}') for a in apps if a['subdomain']=='my-app']"

# Visit the app
# Visit the app (host matches your platform domain, e.g. apps.nzero.pro)
open "https://my-app.apps.privateprompt.tech"
Comment on lines +1703 to 1704
```

### Getting Credentials for Local Development

**N0 API JWT token** (for deploying):
```bash
# NOTE: no trailing slash on /auth/login
curl -s -X POST "$API/auth/login" \
-H "Content-Type: application/json" \
-d '{"email":"you@example.com","password":"..."}' | \
python3 -c "import json,sys; print(json.load(sys.stdin)['data']['tokens']['access_token'])"
```
All credentials come from the single N0 PAT — no email/password, no web UI, no SSH.
See "Authentication (single token, HTTPS-only)" near the top.

**N0 API token**: use `$N0_API_TOKEN` (a `clovr_pat_...` PAT) as `Authorization: Bearer $N0_API_TOKEN` on every call. Do NOT use `/auth/login`.

**Workspace ID** (needed for deploy API calls):
```bash
curl -s "$API/workspaces/" \
-H "Authorization: Bearer $N0_TOKEN" | \
curl -s "$N0_API_BASE/workspaces/" \
-H "Authorization: Bearer $N0_API_TOKEN" | \
python3 -c "import json,sys; ws=json.load(sys.stdin)['data']; [print(f'{w[\"slug\"]}: {w[\"id\"]}') for w in ws]"
```

**Gitea personal access token** (for git push):
- Visit `https://gitea-{workspace-slug}.apps.{platform-domain}/-/user/settings/applications`
- Click "Generate New Token" → select `write:repository, write:package` scopes → copy token
- Or via Gitea API (if you already have a token with `write:user` scope):
```bash
curl -s -X POST "https://${GITEA_HOST}/api/v1/users/{username}/tokens" \
-H "Authorization: token $EXISTING_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "deploy-token", "scopes": ["write:repository", "write:package"]}'
```
**Gitea token** (for repo creation + git push): mint it from the n0 API with the
PAT (requires `gitea:write` scope) — HTTPS-only, sandbox-safe:
```bash
curl -s -X POST "$N0_API_BASE/workspaces/$WS_ID/gitea/token/" \
-H "Authorization: Bearer $N0_API_TOKEN" | \
python3 -c "import json,sys; d=json.load(sys.stdin)['data']; print(f'token={d[\"token\"]}\nuser={d[\"username\"]}\nurl={d[\"gitea_url\"]}')"
```
Do NOT create Gitea tokens via the web UI or via `gitea admin ... generate-access-token` (SSH). Use an HTTPS git remote, never SSH.

**Supabase credentials** (for apps using the workspace Supabase):
- **Via API** (recommended):
```bash
curl -s "$API/workspaces/$WS_ID/supabase/credentials/" \
-H "Authorization: Bearer $N0_TOKEN" | \
curl -s "$N0_API_BASE/workspaces/$WS_ID/supabase/credentials/" \
-H "Authorization: Bearer $N0_API_TOKEN" | \
python3 -c "import json,sys; d=json.load(sys.stdin)['data']; print(f'URL: {d[\"url\"]}\nAnon key: {d[\"anon_key\"]}')"
```
- **URL pattern**: `https://supabase-api-{workspace-slug}.apps.{platform-domain}`
- These are set as `VITE_SUPABASE_URL` and `VITE_SUPABASE_ANON_KEY` in your local `.env` file (gitignored)

**Running Supabase migrations** (for creating tables, RLS policies, etc.):
```bash
curl -s -X POST "$API/workspaces/$WS_ID/supabase/sql/" \
-H "Authorization: Bearer $N0_TOKEN" \
curl -s -X POST "$N0_API_BASE/workspaces/$WS_ID/supabase/sql/" \
-H "Authorization: Bearer $N0_API_TOKEN" \
-H "Content-Type: application/json" \
-d "$(python3 -c "import json; print(json.dumps({'sql': open('migration.sql').read()}))")"
```
Expand Down