Skip to content
Merged
Show file tree
Hide file tree
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

A Claude Code skill for analyzing codebases and generating everything needed to deploy them on [N0](https://nzero.pro) β€” a self-hosted team platform with per-workspace app hosting.

> **This repository is the single source of truth for the n0-app skill.** Copies shipped inside N0 agent sandboxes (`/workspace/skills/n0-app/SKILL.md`) and local installs are mirrors of this repo β€” contribute changes here.

## What it does

When triggered, this skill:

1. **Analyzes** a repository to detect the tech stack, framework, ports, databases, and dependencies
2. **Decides the image strategy**: use upstream pre-built images OR generate a custom `Dockerfile`
2. **Decides the image strategy**: upstream pre-built images, a custom `Dockerfile`, OR a zero-build `config_files` app (no CI needed)
3. **Generates** an `n0-app.json` manifest describing all services
4. **Generates** a `.gitea/workflows/build-and-push.yml` CI workflow for building and pushing Docker images
5. **Validates** the output against N0's manifest schema
Comment on lines 11 to 15
Expand Down
83 changes: 79 additions & 4 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ user-invocable: true
argument-hint: "[optional: path to repo or description of the app]"
---

<!--
SOURCE OF TRUTH: https://github.com/Lunar-Rails/n0-app-skill
All other copies (N0 sandbox template django_backend/sandbox/templates/skills/n0-app/,
local ~/.claude/skills installs) are mirrors. Edit on GitHub, then sync mirrors.
-->

# N0 App Skill

Analyze a codebase and generate everything needed to deploy it as a hosted app on N0.
Expand Down Expand Up @@ -104,7 +110,7 @@ git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/ori

### Step 2: Decide the Image Strategy

**Choose ONE of these two paths:**
**Choose ONE of these three paths:**

#### Path A: Use Upstream Pre-built Images (preferred when available)
Comment on lines 111 to 115

Expand Down Expand Up @@ -144,6 +150,52 @@ Use this path when:

Continue to Step 2b below.

#### Path C: Zero-Build App (config_files β€” fastest iteration, no CI)

Use this path when:
- The app is a small/medium single-service app (Node/Python script + static assets)
- You want the fastest possible edit β†’ deploy loop (no Docker build, no registry, no CI wait)
- Total source size fits comfortably under **1 MiB** (Kubernetes ConfigMap limit β€” includes JSON overhead; keep the embedded payload under ~900 KB)

**How it works:** use a stock public image (e.g. `node:20-alpine`, `python:3.12-alpine`) and embed the entire app source in `config_files` on a volume. The platform writes the files into the volume before the container starts, and `command` runs them directly:

```json
{
"services": {
"web": {
"image": "node:20-alpine",
"port": 3000,
"command": ["node", "/app/server.js"],
"volumes": {
"app": {"mount": "/app"},
"data": {"mount": "/data", "persistent": true}
},
"config_files": {
"app": {
"server.js": "...entire file contents...",
"index.html": "...entire file contents..."
}
}
}
}
}
```

Key points:
- **No Dockerfile, no Gitea Actions workflow, no registry** β€” skip Steps 2b and 5 entirely
- Store app state in a **persistent volume** (e.g. `/data/db.json`) β€” the config_files volume is recreated on every deploy
- Binary assets (images, audio) can be embedded as base64 strings and decoded by the server at startup
- The deploy loop is: edit source files β†’ **regenerate `n0-app.json`** (script that reads the files and patches `services.web.config_files.app`) β†’ push both to Gitea β†’ re-import the definition (`POST .../apps/definitions/` with `{"repo_url": ...}`) β†’ `POST .../apps/{id}/redeploy` with `{}`
- **Never hand-edit the embedded copies inside n0-app.json** β€” always regenerate from the real source files, e.g.:
```python
import json
m = json.load(open("n0-app.json"))
cf = m["services"]["web"]["config_files"]["app"]
for f in ("server.js", "index.html"):
cf[f] = open(f).read()
json.dump(m, open("n0-app.json", "w"), indent=2)
```

### Step 2b: Generate the Dockerfile

Create a multi-stage Dockerfile optimized for the detected stack.
Expand Down Expand Up @@ -1443,9 +1495,13 @@ 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. **Workspace member JWT token or Personal Access Token** β€” any workspace member 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
3. **Gitea Actions build completed** β€” the Docker image must be in the registry before deploy (not needed for Path C zero-build apps)

**API response envelope:** all N0 API responses are wrapped as `{"success": true, "data": {...}}` β€” read fields from `data`, not the top level.

**Verifying a deployed (workspace-gated) app from the CLI:** apps behind SSO can be curled by appending an iframe token: `TOK=$(curl -s -H "Authorization: Bearer $TOKEN" "https://app.../api/v1/apps/iframe-token" | python3 -c "import json,sys;print(json.load(sys.stdin)['data']['token'])")` then `curl "https://my-app.apps.../?_ppauth=$TOK"`. Add a cache-buster query param when verifying fresh deploys.

### Step 1: Import App Definition

Expand Down Expand Up @@ -1493,11 +1549,30 @@ The deploy is async β€” a background Huey task:

### Step 3: Redeploy (after code changes)

For zero-build (config_files) apps, **re-import the definition first** so the platform picks up the new `n0-app.json` from Gitea, then redeploy:

```bash
# 1. Re-import (refreshes AppDefinition.manifest + source commit)
curl -s -X POST "https://app.privateprompt.tech/api/v1/workspaces/${WS_ID}/apps/definitions/" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"repo_url": "clovrlabs/my-app"}'

# 2. Redeploy the running instance
curl -s -X POST "https://app.privateprompt.tech/api/v1/workspaces/${WS_ID}/apps/${APP_ID}/redeploy" \
-H "Authorization: Bearer $TOKEN"
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{}'
```

### Deploy Versioning & Rollback

**Every deploy gets a unique version tag** β€” never rely on `latest` as a version identifier:

- The platform tags each deploy record with the app definition's **source commit short-sha** (or a `vYYYYMMDD-HHMMSS` timestamp when no commit is available) and stores a **full manifest snapshot** on the deploy record
- The Studio UI shows the deployed version and the per-deploy tags in Deploy History
- **Rollback**: `POST .../apps/${APP_ID}/redeploy` with `{"record_id": "<deploy-record-id>"}` restores that record's manifest snapshot (config_files, images, env) and redeploys it β€” a true rollback, not just a re-pull
- Only pass `{"image_tag": "..."}` when you explicitly want to deploy a different **image** tag of a custom-built image; version labels are NOT image tags

Practical rule for zero-build apps: since the "version" lives in the manifest (ConfigMaps), always push a Gitea commit per change and re-import before redeploying β€” that commit sha becomes the deploy's version tag and enables rollback.

### Other Operations

```bash
Expand Down