← VolcanoCONTENT HISTORY

Update to Volcano

Snapshot Oct 24, 2026 · 13:36 UTC · version 0.2.26

Collection source: downloaded plugin package.

WHAT CHANGED

First saved snapshot

No earlier snapshot is available to establish a change.

Save Volcano and check future changes in Following.

Up to 10 plugins free · no card required.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "description": "Use for every Volcano project build, scaffold, local run, test, or deploy. Covers canonical project shape, Functions and frontends, migrations, volcano-config.yaml, environment variables, shared code, and the build and deploy workflow. Pair with volcano-sdk for application features.",
  "included_files": [],
  "name": "volcano-platform",
  "skill_md_contents": "---\nname: volcano-platform\ndescription: Use for every Volcano project build, scaffold, local run, test, or deploy. Covers canonical project shape, Functions and frontends, migrations, volcano-config.yaml, environment variables, shared code, and the build and deploy workflow. Pair with volcano-sdk for application features.\n---\n# Volcano Platform Contract Skill\n\n## Before anything else: check the Volcano CLI\n\nRun `which volcano` and\n`env -u VOLCANO_GITHUB_RELEASES_URL -u VOLCANO_CLI_RELEASES_URL volcano --version`.\nIf a working CLI is installed,\nrecord its version and continue without upgrading it. Keep both release-source\noverrides cleared for every CLI invocation and npm install in this workflow,\nincluding deferred binary downloads. If it is missing, read\nthe bundled `../install-volcano/SKILL.md` and follow its exact-version package\nverification procedure. Do not fetch and execute installation instructions,\nrun remote shell installers, or use a moving release or branch as an executable\nsource. Only upgrade when the user requests it, using the same verified,\nversioned installation procedure. If installation cannot be verified, report\nthe prerequisite and continue only with independent work.\n\n## Role & Pairing\nDefines the canonical project shape and deploy contract for any Volcano-Hosting-deployable codebase. Covers the `volcano/functions/` deployment model, migrations, `volcano-config.yaml`, environment variables, shared-code conventions, and the build/deploy workflow.\n\n**Pair with:**\n- `volcano-sdk` — the SDK entrypoint and skill router.\n- `volcano-functions` — function-writing internals: handler templates, invocation contract, user-context patterns, error handling.\n\nThis skill focuses on **project shape and deploy mechanics**, not on how to write individual function handlers.\n\n## Relationship to `volcano init`\n\n`volcano init` (from the Volcano CLI) creates a minimal runtime skeleton in a `volcano/` directory:\n\n```\nvolcano/                        # Volcano-managed project directory\nvolcano/.gitignore              # ignores volcano.env, .env.local, logs\nvolcano/volcano.env             # local environment variables (gitignored)\nvolcano/volcano.env.example     # env var documentation (committed)\nvolcano/migrations/             # SQL migration files\nvolcano/migrations/README.md    # migration conventions\nvolcano/README.md               # next-step instructions\n```\n\nTemplates add language-specific files on top of the base scaffold:\n- `volcano init nextjs` (aliases: `next`, `next.js`, `next-js`) — adds a minimal Next.js app under `web/` plus example functions and migrations.\n- `volcano init javascript` (aliases: `js`, `node`, `nodejs`) — adds a starter function handler and `volcano-config.yaml`.\n- `volcano init python` (alias: `py`) — adds a Python function handler.\n- `volcano init ruby` (alias: `rb`) — adds a Ruby function handler.\n\nAdd `--example` for a more complete demo: `volcano init nextjs --example notes`, or `volcano init javascript|python|ruby --example hello-world`.\n\nNote: `volcano-config.yaml` is created only by the `javascript` template, not by the base scaffold. You can also create it manually at `volcano/volcano-config.yaml`.\n\n`volcano init` does **not** create an `src/api/` directory, an OpenAPI dispatcher, or a single bundled entry point. Functions deploy individually from `volcano/functions/`.\n\n## Project Layout (canonical)\n\n```\n.\n├── volcano/                        # Volcano-managed directory (created by volcano init)\n│   ├── functions/                  # Function handlers (deployment root)\n│   │   ├── hello.js                # → deploys as function \"hello\"\n│   │   ├── notes-summary.js        # → deploys as function \"notes-summary\"\n│   │   ├── api/                    # → deploys as function \"api\" (directory function)\n│   │   │   └── index.js            # entry point inside a directory function\n│   │   └── _shared/                # shared code (underscore-prefixed, auto-bundled)\n│   │       └── volcano-client.js   # canonical client factory\n│   ├── migrations/\n│   │   ├── 001_init.sql            # numeric-prefix alphabetical ordering\n│   │   └── 002_add_posts.sql\n│   ├── volcano-config.yaml         # declarative project config (see \"volcano-config.yaml\" below)\n│   ├── volcano.env                 # local env vars (gitignored)\n│   ├── volcano.env.example         # env var documentation (committed)\n│   └── .gitignore\n├── package.json                    # optional — needed only for the build model\n├── tsconfig.json                   # optional — needed only for the build model\n└── src/                            # optional — authoring source for the build model\n    ├── functions/\n    │   ├── hello.ts\n    │   └── notes-summary.ts\n    └── lib/\n        └── volcano-client.ts\n```\n\n**Hard rules:**\n- Function handlers live ONLY under `volcano/functions/`.\n- Migrations live ONLY under `volcano/migrations/`.\n- Shared code that functions import at runtime uses the `_`-prefix convention (`_shared/`, `_lib/`) so the scanner skips it as a function candidate but the packager bundles it.\n- `volcano-config.yaml` is the declarative config for the full project (project settings, databases, variables, buckets/policies, realtime, auth, functions/schedulers, frontends) — see \"volcano-config.yaml\" below, not just buckets and function visibility.\n\n## Function Deployment Model\n\n`volcano functions deploy` scans `volcano/functions/` and deploys each handler as an individual function.\n\n**Scanner rules** (source: `volcano-cli/internal/function/scanner.go`):\n- Scans ONLY the `volcano/functions/` directory (hard-coded path).\n- **One function per file:** `hello.js` → function named `hello`.\n- **One function per directory:** `api/index.js` → function named `api` (the directory name).\n- Entries starting with `_` are **skipped** as function candidates but bundled as shared code when imported.\n- Supported runtimes: `nodejs22.x` (minimum), `nodejs24.x` (default). Also Python and Ruby.\n\nThere is **no router, no OpenAPI dispatcher, no single entry point**. Each file is an independent function with its own URL.\n\n## Event & Response Shape\n\n### Event (IN)\nFunctions receive the **bare caller payload** as `event`. There is no `httpMethod`, `path`, or `queryStringParameters`. If the caller invokes with `{ name: 'Alice' }`, the handler receives exactly that object.\n\nWhen the request carries a valid Bearer token **and the payload is an object/map**, Volcano injects `__volcano_auth` into the event:\n\n```ts\ntype VolcanoAuth = {\n  user_id: string;\n  email: string;\n  project_id: string;\n  role: string;\n  access_token: string;\n};\n```\n\n**Caveat:** if the payload is not an object (string, number, array), `__volcano_auth` is **not** injected — the payload passes through unchanged. Always guard for its absence.\n\n### Response (OUT)\n```ts\ntype FunctionResponse = {\n  statusCode: number;\n  body: string;              // always JSON.stringify(...) for JSON responses\n  headers?: Record<string, string>;\n};\n```\n\n**Use `statusCode: 200` for every successful response — not `201`, `202`, or\nany other 2xx.** `volcano functions invoke` reports any non-200 status as a\nfailed invocation (e.g. `Error: failed to invoke function \"x\": HTTP 201`),\nso returning `201 Created` for a create makes the CLI — and an agent reading\nits output — treat a working function as broken and thrash trying to \"fix\"\nit. Return `200` with the created/updated resource in the `body`; reserve\nnon-2xx for real failures (`400` bad input, `401` unauthenticated, `500`\ninternal).\n\n### Handler signature\n```js\nexports.handler = async (event) => {\n  const auth = event && typeof event === 'object' ? event.__volcano_auth : undefined;\n  // ...\n  return { statusCode: 200, body: JSON.stringify({ ok: true }) };\n};\n```\n\nFor detailed handler templates, invocation patterns, and user-context guidance, see the `volcano-functions` skill.\n\n## Authoring Models\n\n### Model A — Native JavaScript (what `volcano init` scaffolds)\n\nAuthor `.js` handlers directly in `volcano/functions/`. Share code via `volcano/functions/_shared/` using the underscore-prefix convention. The packager bundles `_shared/` into each function archive alongside the entrypoint, so `require(\"./_shared/...\")` resolves at runtime.\n\n```js\n// volcano/functions/hello.js\nexports.handler = async (event) => {\n  const name = event?.name || 'World';\n  return {\n    statusCode: 200,\n    body: JSON.stringify({ message: `Hello, ${name}!` }),\n  };\n};\n```\n\n```js\n// volcano/functions/_shared/volcano-client.js  — shared across functions\nconst { VOLCANO_API_URL, VOLCANO_ANON_KEY, VOLCANO_DATABASE = 'app' } = process.env;\n\nfunction createVolcanoClient(VolcanoAuthClass, { apiUrl, anonKey, database = 'app', accessToken }) {\n  if (!apiUrl || !anonKey) throw new Error('Missing VOLCANO_API_URL or VOLCANO_ANON_KEY');\n  const volcano = new VolcanoAuthClass({ apiUrl, anonKey, ...(accessToken && { accessToken }) });\n  return volcano.database(database);\n}\n\nconst createFunctionVolcanoClient = (VolcanoAuthClass, auth) => {\n  if (!auth?.access_token) throw new Error('Missing function auth access token');\n  return createVolcanoClient(VolcanoAuthClass, {\n    apiUrl: VOLCANO_API_URL,\n    anonKey: VOLCANO_ANON_KEY,\n    database: VOLCANO_DATABASE,\n    accessToken: auth.access_token,\n  });\n};\n\nmodule.exports = { createVolcanoClient, createFunctionVolcanoClient };\n```\n\n```js\n// volcano/functions/notes-summary.js  — uses shared client\nconst { createFunctionVolcanoClient } = require('./_shared/volcano-client');\n\nlet VolcanoAuthClass;\nasync function getVolcanoAuthClass() {\n  if (!VolcanoAuthClass) {\n    const sdk = await import('@volcano.dev/sdk');\n    VolcanoAuthClass = sdk.VolcanoAuth || sdk.default;\n  }\n  return VolcanoAuthClass;\n}\n\nexports.handler = async (event) => {\n  const input = event && typeof event === 'object' ? event : {};\n  const auth = input.__volcano_auth;\n  if (!auth?.access_token) {\n    return { statusCode: 401, body: JSON.stringify({ error: 'Unauthorized' }) };\n  }\n  const VolcanoAuth = await getVolcanoAuthClass();\n  const volcano = createFunctionVolcanoClient(VolcanoAuth, auth);\n  const { data, error } = await volcano.from('notes').select('id,title').limit(5);\n  if (error) return { statusCode: 500, body: JSON.stringify({ error: error.message }) };\n  return { statusCode: 200, body: JSON.stringify({ notes: data ?? [] }) };\n};\n```\n\n**A Model A function that imports an npm package (including `@volcano.dev/sdk`, as `notes-summary.js` does) MUST declare it in a `package.json`, or the import fails at runtime with `Cannot find package '@volcano.dev/sdk'`.** `volcano functions deploy` uploads dependency *manifests* (`package.json`) and lets the runtime install from them; it deliberately never uploads your local `node_modules` (it's excluded from the archive). Declaring the import is not enough on its own — there must be a manifest listing it:\n\n```json\n// volcano/functions/package.json  — declares deps for the native-JS functions\n{\n  \"name\": \"functions\",\n  \"private\": true,\n  \"dependencies\": { \"@volcano.dev/sdk\": \"latest\" }\n}\n```\n\nThe scanner skips `package.json`/`node_modules` as function candidates (they are not handlers), so this file sits alongside your `.js` handlers without becoming its own function. `hello.js` above needs no manifest because it imports nothing.\n\n### Model B — Build-into-functions (TypeScript)\n\nAuthor TypeScript in `src/functions/<name>.ts`, bundle each to `volcano/functions/<name>.js` with esbuild, then deploy. This lets you use TypeScript, `src/lib/` shared code, and npm dependencies that esbuild bundles.\n\n**`package.json`:**\n```json\n{\n  \"name\": \"my-volcano-project\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"build:functions\": \"esbuild src/functions/*.ts --bundle --platform=node --target=node22 --format=cjs --outdir=volcano/functions\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@volcano.dev/sdk\": \"latest\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^22.10.2\",\n    \"esbuild\": \"^0.24.0\",\n    \"typescript\": \"^5.7.2\"\n  }\n}\n```\n\n**`tsconfig.json`:**\n```json\n{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"CommonJS\",\n    \"moduleResolution\": \"Node\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"noEmit\": true,\n    \"lib\": [\"ES2022\"],\n    \"types\": [\"node\"]\n  },\n  \"include\": [\"src/**/*.ts\"]\n}\n```\n\n**Source handler (`src/functions/hello.ts`):**\n```ts\nimport { createFunctionVolcanoClient } from '../lib/volcano-client';\n\nexport const handler = async (event: { name?: string; __volcano_auth?: { access_token?: string } }) => {\n  const name = event.name || 'World';\n  return { statusCode: 200, body: JSON.stringify({ message: `Hello, ${name}!` }) };\n};\n```\n\nAfter `npm run build:functions`, the output `volcano/functions/hello.js` is what deploys.\n\n**Critical:** `volcano functions deploy` does NOT run your build. The built `.js` files must already exist under `volcano/functions/` on disk. Always run `npm run build:functions` before deploying. Committing the built output is the safest approach — the packager applies project ignore rules when bundling directory contents and shared libraries, so gitignoring `volcano/functions/` can cause packaging edge cases.\n\n## Canonical Shared Client Pattern\n\nBoth models use the same factory pattern, adapted from the `nextjs-notes` starter (`volcano/functions/_shared/volcano-client.js`):\n\n| Factory | When to use |\n|---|---|\n| `createFunctionVolcanoClient(VolcanoAuthClass, auth)` | Inside function handlers — uses `__volcano_auth.access_token` |\n| `createWebVolcanoClient(VolcanoAuthClass)` | Browser/Next.js — uses `NEXT_PUBLIC_*` env vars |\n\nBoth read `VOLCANO_API_URL`, `VOLCANO_ANON_KEY`, and `VOLCANO_DATABASE` from `process.env` and fall back to database name `'app'`.\n\nFor browser-side client setup, see the `volcano-nextjs` skill.\n\n## Environment Variables\n\nFunctions receive **only user-defined project variables**. Volcano does **not** auto-inject `VOLCANO_API_URL`, `VOLCANO_ANON_KEY`, or `VOLCANO_DATABASE` into the function runtime. You must define these as project variables.\n\n**Deploy variables:**\n```sh\nvolcano variables deploy\n```\nThis reads from `volcano/volcano.env` (or a specified file) and sets project-scoped environment variables that all functions in the project receive at runtime.\n\n**Canonical variable names** (the shared client factory expects these):\n| Variable | Purpose |\n|---|---|\n| `VOLCANO_API_URL` | Volcano API endpoint |\n| `VOLCANO_ANON_KEY` | Public anon key |\n| `VOLCANO_DATABASE` | Database name (defaults to `'app'` if unset) |\n\n**Custom secrets** (any name): `STRIPE_SECRET_KEY`, `SENDGRID_API_KEY`, `SLACK_WEBHOOK_URL`, etc.\n\n**`volcano/volcano.env.example`** (committed for documentation):\n```env\nVOLCANO_API_URL=https://api.volcano.dev\nVOLCANO_ANON_KEY=your-anon-key\nVOLCANO_DATABASE=app\n\n# Custom secrets (add as needed)\n# STRIPE_SECRET_KEY=sk-...\n```\n\nNever hardcode secrets in handler code.\n\n**`DATABASE_URL` is auto-injected** (unlike `VOLCANO_API_URL`/`VOLCANO_ANON_KEY`/`VOLCANO_DATABASE` above) and carries full admin access (`application_name=volcano_full_access`) by default. Always use the SDK client (`volcano.from(...)`), not `DATABASE_URL`. Direct Postgres access is a discouraged last resort with untested, unbounded surface area — see the `volcano-database` skill's \"Direct Postgres Access\" section before ever reaching for it.\n\n## Migrations & Row-Level Security\n\n### Migration conventions\n- **Location:** `volcano/migrations/` (no subdirectories).\n- **Filename:** `NNN_description.sql` — numeric prefix for alphabetical ordering (e.g., `001_init.sql`, `002_add_posts.sql`). Ordering is alphabetical; the numeric prefix is a convention, not enforced.\n- **Idempotency:** use `CREATE TABLE IF NOT EXISTS`, `DROP POLICY IF EXISTS` before `CREATE POLICY`, and `CREATE OR REPLACE FUNCTION`.\n- **One statement per file:** `volcano migrations deploy` executes each file as a single SQL\n  statement — it does not split on `;` or batch multiple statements, and\n  rejects a multi-statement body outright (`ERROR: Multiple statements are\n  not supported`). Do NOT wrap a file in `BEGIN; ... COMMIT;`, and do NOT put\n  more than one statement in one file. A schema change that needs several\n  statements (create table, add indexes, enable RLS, add policies) is\n  several sequentially-numbered files, not one — see \"Canonical migration\n  with RLS\" below.\n\n**Deploy:**\n```sh\nvolcano migrations deploy --all -d app\n```\n\n### Auth helper functions\n\nVolcano automatically installs these SQL helper functions in **every project database** (source: `volcano-hosting/internal/database/auth_helpers.go`). They read JWT claims from PostgreSQL session variables set by the request pipeline.\n\n| Helper | Session variable | Returns |\n|---|---|---|\n| `auth.uid()` | `request.jwt_sub` | `UUID` — authenticated user's id (or NULL) |\n| `auth.email()` | `request.jwt_email` | `TEXT` — authenticated user's email |\n| `auth.role()` | `request.jwt_role` | `TEXT` — `'authenticated'`, `'anonymous'`, or `'anon'` (see below) |\n| `auth.is_authenticated()` | (derived) | `BOOLEAN` — `auth.uid() IS NOT NULL` |\n\n**Important:** the schema prefix `auth.` is required. Write `auth.uid()`, not bare `uid()`.\n\n**`auth.role()` has three distinct values, not two:**\n- `'authenticated'` — a real signed-up user (email/password or OAuth).\n- `'anonymous'` — a guest user created via `signUpAnonymous()`. They DO have a valid JWT and a real `auth.uid()`; RLS still applies to them like any authenticated caller.\n- `'anon'` — no token at all (the SQL function's fallback default, and also the underlying Postgres role name for unauthenticated access).\n\nA policy that checks `auth.role() = 'anon'` to gate out guests is a common mistake — guest sessions report `'anonymous'`, not `'anon'`. Prefer `auth.uid() IS NOT NULL` (or `auth.is_authenticated()`) when the intent is \"any signed-in caller, including guests.\"\n\n### Canonical migration with RLS\n\nOne statement per file, numbered sequentially — each `-- <filename>` comment\nbelow marks a separate file, not a section of one file:\n\n```sql\n-- 001_create_posts_table.sql\nCREATE TABLE IF NOT EXISTS posts (\n    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n    user_id UUID NOT NULL DEFAULT auth.uid(),\n    title TEXT NOT NULL,\n    content TEXT NOT NULL DEFAULT '',\n    status TEXT NOT NULL DEFAULT 'draft',\n    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\n\n-- 002_posts_user_id_index.sql\nCREATE INDEX IF NOT EXISTS posts_user_id_idx ON posts(user_id);\n\n-- 003_posts_created_at_index.sql\nCREATE INDEX IF NOT EXISTS posts_created_at_idx ON posts(created_at DESC);\n\n-- 004_posts_enable_rls.sql\nALTER TABLE posts ENABLE ROW LEVEL SECURITY;\n\n-- 005_posts_policy_select_own_drop.sql\nDROP POLICY IF EXISTS posts_select_own ON posts;\n\n-- 006_posts_policy_select_own_create.sql\nCREATE POLICY posts_select_own ON posts\n    FOR SELECT USING (user_id = auth.uid());\n\n-- 007_posts_policy_insert_own_drop.sql\nDROP POLICY IF EXISTS posts_insert_own ON posts;\n\n-- 008_posts_policy_insert_own_create.sql\nCREATE POLICY posts_insert_own ON posts\n    FOR INSERT WITH CHECK (user_id = auth.uid());\n\n-- 009_posts_policy_update_own_drop.sql\nDROP POLICY IF EXISTS posts_update_own ON posts;\n\n-- 010_posts_policy_update_own_create.sql\nCREATE POLICY posts_update_own ON posts\n    FOR UPDATE USING (user_id = auth.uid())\n    WITH CHECK (user_id = auth.uid());\n\n-- 011_posts_policy_delete_own_drop.sql\nDROP POLICY IF EXISTS posts_delete_own ON posts;\n\n-- 012_posts_policy_delete_own_create.sql\nCREATE POLICY posts_delete_own ON posts\n    FOR DELETE USING (user_id = auth.uid());\n```\n\nFor public-read patterns, add a policy (its own file) with `USING (status = 'published')` alongside the owner policies.\n\n## volcano-config.yaml\n\nDeclarative configuration for the full project: project settings, database\nassertions, variables, buckets/policies, realtime, auth (providers, email,\ntemplates, managed pages), function visibility/schedulers, and frontend\ncustom domains. Located at `volcano/volcano-config.yaml` or root\n`volcano-config.yaml`. Only declared sections are reconciled — everything\nelse is left untouched.\n\n`pull` exports the target's current configuration to a file; `deploy`\nuploads a file and reconciles the target to match it. Only the command\nnamespace changes between local and cloud:\n\n| Target | Export to file | Apply from file |\n|---|---|---|\n| Local (`volcano start`) | `volcano config pull` | `volcano config deploy` |\n| Cloud (`volcano login` + `volcano use`) | `volcano cloud config pull` | `volcano cloud config deploy` |\n\nUse `pull` to seed a manifest from an existing project instead of\nhand-writing one from scratch, and `deploy --dry-run` to preview reconcile\nactions before applying.\n\n**Pull exports include plaintext variable values, but not other secrets.**\nWrite-only fields (SMTP password, OAuth client secrets, custom domain TLS\nmaterial) are omitted from the export and stay unchanged unless set\nexplicitly, but `variables[].value` **is** included in the clear. Never\ncommit a pulled manifest as-is: replace secret values with `${ENV_VAR}`\nreferences (interpolated from the CLI environment before upload) before it\ntouches version control.\n\n**A declared `variables` section is a second, competing source of truth for\nvariables — don't run both.** If a manifest declares `variables`, `config\ndeploy` fully syncs the project's variables to exactly that list, deleting\nany variable not listed. That conflicts with `volcano variables deploy` /\n`volcano cloud variables deploy` (see \"Environment Variables\" above) if both\nare used for the same variables: whichever runs last wins, and `config\ndeploy` will delete anything only `variables deploy` set. Pick one path for\nvariables per project — typically `variables deploy` from `volcano.env` for\nday-to-day secrets, and only add `variables` to the manifest if you want it\nto be the single source of truth instead.\n\n**Partial example** (see the full schema at\n`volcano-hosting/docs/projects/configuration.md` — it also covers `project`,\n`databases`, `realtime`, `auth`, and `frontends`, omitted here for brevity):\n```yaml\nversion: 1                          # required — only version 1 is supported\n\nvariables:\n  - name: STRIPE_SECRET_KEY\n    value: ${STRIPE_SECRET_KEY}     # interpolated from the CLI environment\n\nbuckets:                            # bucket must already exist — never created here\n  - name: avatars\n    file_size_limit: 5242880        # optional — bytes\n    allowed_mime_types: [image/jpeg, image/png]\n    policies:                       # fully synced when declared — omit the key to leave untouched\n      - name: owner-read-write\n        operation: SELECT           # SELECT, INSERT, UPDATE, or DELETE\n        definition: \"auth.uid() IS NOT NULL\"\n\nfunctions:                          # function must already be deployed — never created here\n  - name: notes-summary\n    public: false                   # required — boolean\n    schedulers:                     # fully synced when declared\n      - name: refresh-cache\n        cron: \"*/5 * * * *\"\n        enabled: true\n        payload: { job: refresh }\n```\n\n**Hard rules:**\n- `version: 1` is required. It's a schema version, not a Terraform-style\n  state serial — it never needs bumping between deploys.\n- Functions, frontends, databases, and buckets are **never created or\n  deleted** through the manifest — they must already exist. A declared entry\n  for a resource that doesn't exist is reported `skipped`; a deployed\n  resource missing from a declared section is reported `missing`. Both are\n  non-fatal warnings.\n- Omitted sections/fields are left untouched (patch semantics). An **empty\n  declared list** for a fully-synced collection — `variables`,\n  `buckets[].policies`, `auth.providers.oauth`, `auth.email.templates`,\n  `functions[].schedulers` — deletes everything currently in it. These are\n  destructive by design; declaring the section at all means it's the source\n  of truth.\n- Each bucket policy `operation` must be `SELECT`, `INSERT`, `UPDATE`, or\n  `DELETE`. Each declared function needs `public` set (boolean).\n- Function visibility can also be set imperatively via\n  `volcano cloud functions update --public` / `--private`; `functions deploy`\n  itself does **not** read `volcano-config.yaml`.\n\n**There is no state file.** Don't invent one, and don't build a manual\nrollback step for a failed `config deploy` — neither exists or is needed:\n- Every `config deploy` (`--dry-run` included) diffs the manifest against the\n  project's live configuration at request time. There's no cached snapshot of\n  a prior apply to reconcile against.\n- A failed apply still attempts every other planned change and reports\n  `created` / `updated` / `deleted` / `unchanged` / `error` per entry;\n  already-applied changes are **not** rolled back.\n- Re-running `config deploy` with the *same* file is always safe: entries\n  that already landed report `unchanged`; only entries that failed or still\n  differ produce a new action. Fix the underlying issue (bad cron expression,\n  plan-gated field, etc.) and re-run the same command — don't try to track or\n  hand-edit applied state anywhere.\n\n## Deploy & Local-Dev Workflow\n\n### Local development\n```sh\nvolcano start                       # start local Volcano stack (API, DB, functions runtime)\n```\nThe local stack reads `volcano/volcano.env` for environment variables. Functions can be invoked locally through the local API endpoint.\n\n### Local deploy sequence\n```sh\n# 0. (optional, only if volcano-config.yaml doesn't exist yet) seed it from\n#    the current project state — pull refuses to overwrite an existing file\n#    without --force, so skip this if a manifest is already present (for\n#    example the one `volcano init javascript` scaffolds).\n#    WARNING: a pulled manifest contains plaintext variable values AND a\n#    `variables` section. Before step 4, (a) replace secret values with\n#    ${ENV_VAR} references — never commit a pulled manifest as-is — and\n#    (b) delete the `variables` section unless you intend the manifest to be\n#    the single source of truth, otherwise step 4 (`config deploy`) fully\n#    syncs variables to that list and deletes anything only step 2\n#    (`variables deploy`) set. See the \"volcano-config.yaml\" warnings above.\nvolcano config pull\n\n# 1. Build function output (Model B only — skip for native JS)\nnpm run build:functions\n\n# 2. Deploy environment variables\nvolcano variables deploy\n\n# 3. Deploy functions from volcano/functions/\nvolcano functions deploy --all\n\n# 4. Reconcile all declared config sections (project, databases, variables,\n#    buckets/policies, realtime, auth, function visibility/schedulers, frontends)\nvolcano config deploy\n\n# 5. Apply database migrations\nvolcano migrations deploy --all -d app\n```\n\n### Cloud deploy (requires `volcano login` + `volcano use`)\n```sh\n# 0. (optional, only if volcano-config.yaml doesn't exist yet) seed it from\n#    the current project state — pull refuses to overwrite an existing file\n#    without --force, so skip this if a manifest is already present (for\n#    example the one `volcano init javascript` scaffolds).\n#    WARNING: a pulled manifest contains plaintext variable values AND a\n#    `variables` section. Before step 4, (a) replace secret values with\n#    ${ENV_VAR} references — never commit a pulled manifest as-is — and\n#    (b) delete the `variables` section unless you intend the manifest to be\n#    the single source of truth, otherwise step 4 (`cloud config deploy`)\n#    fully syncs variables to that list and deletes anything only step 2\n#    (`cloud variables deploy`) set — on cloud this is production data loss.\n#    See the \"volcano-config.yaml\" warnings above.\nvolcano cloud config pull\n\n# 1. Build function output (Model B only — skip for native JS)\nnpm run build:functions\n\n# 2. Deploy environment variables\nvolcano cloud variables deploy\n\n# 3. Deploy functions from volcano/functions/\nvolcano cloud functions deploy --all\n\n# 4. Reconcile all declared config sections (project, databases, variables,\n#    buckets/policies, realtime, auth, function visibility/schedulers, frontends)\nvolcano cloud config deploy\n\n# 5. Apply database migrations\nvolcano cloud migrations deploy --all -d app\n```\n\n**Order matters:** variables before functions (so handlers have env vars on first deploy), config after functions (so visibility targets exist), migrations last (schema is ready for runtime queries). Note that step 4 (`config deploy`) runs *after* step 2 (`variables deploy`): if the manifest declares a `variables` section, step 4 wins and will overwrite or delete variables set by step 2 — keep `variables` out of the manifest unless it is the single source of truth (see \"volcano-config.yaml\" above).\n\n## Verify a local deploy (smoke test)\n\nDeploying is not the finish line — exercise each function and assert on the\nresult (this is `AGENTS.md`'s \"exercise what was built\" step). Don't report\nsuccess from the deploy output alone.\n\n**One-line smoke test — `volcano functions invoke`.** In local mode this runs the\nfunction *as the pre-provisioned local user*, so `event.__volcano_auth` is\npopulated (id `11111111-…`, `clearwater@volcano.dev`). It's the fastest way to\nconfirm a function deployed, runs, and receives an auth context:\n\n```sh\nvolcano functions invoke <name> --payload '{\"title\":\"Buy milk\"}' --json\n```\n\nCheck the printed status/body. A missing dependency, a broken handler, or a\nbad payload shape surfaces here immediately.\n\n**Caveat — `auth.uid()` is NULL under this harness, so don't judge RLS/owner\nlogic by it.** `volcano functions invoke` is a credential-free local harness\n(see its `--help`): it populates `event.__volcano_auth`, but it does **not**\nestablish a database auth session, so SQL `auth.uid()` returns null here. A\nfunction that inserts an owner-scoped row through a `user_id` column defaulting\nto `auth.uid()`, or whose table has RLS policies using `auth.uid()`, will fail\nwith `null value in column \"user_id\"` or return zero rows when invoked this way\n— that is the **harness context, not a bug in the function**, and it is not a\nreason to rewrite a correct handler (e.g. do NOT start hand-setting `user_id`\njust to satisfy the CLI). Verify per-user / RLS behavior with the SDK recipe\nbelow (a real signed-in user), where `auth.uid()` resolves — exactly as it does\nfor real callers in the browser/app and in the cloud.\n\n**Multi-user / RLS checks — mint real users with the SDK.** The CLI invoke is\nalways that single local user, so it can't verify per-user isolation (that user\nA cannot see user B's rows). For that, sign up throwaway users with the SDK and\ninvoke as each. Read `apiUrl`/`anonKey` from `volcano status`:\n\n```js\n// verify.mjs — run: node verify.mjs   (npm i @volcano.dev/sdk)\nimport { VolcanoAuth } from '@volcano.dev/sdk';\n\nconst apiUrl = 'http://localhost:8000';                 // from `volcano status`\nconst anonKey = 'ak-0000000000000000000000000000000000000000'; // from `volcano status`\n\nasync function tokenFor(email) {\n  const auth = new VolcanoAuth({ apiUrl, anonKey });\n  // Local dev has no email confirmation, so signInWhenAllowed returns a live\n  // session directly; fall back to signIn if it doesn't.\n  let { session } = await auth.signUp({ email, password: 'Smoke-Test-1!', signInWhenAllowed: true });\n  if (!session) ({ session } = await auth.signIn({ email, password: 'Smoke-Test-1!' }));\n  if (!session?.access_token) throw new Error(`no session for ${email}`);\n  return session.access_token;\n}\n\nconst token = await tokenFor(`smoke-${Date.now()}@example.com`);\nconst volcano = new VolcanoAuth({ apiUrl, anonKey, accessToken: token });\nconst { data, status, error } = await volcano.functions.invoke('<name>', { /* payload */ });\nconsole.log({ status, data, error });\nif (error || status < 200 || status >= 300) process.exit(1);\n```\n\nEasy wrong guesses to avoid (all verified against the SDK):\n- The methods are `auth.signUp` / `auth.signIn` — there is no `signInWithPassword`.\n- Auth responses are `{ user, session, error }` at the **top level**; the token\n  is `session.access_token`, not `data.session.access_token`.\n- `functions.invoke(name, payload)` returns `{ data, status, headers, version,\n  error }` — check `error` and `status` before trusting `data`.\n\n## End-to-end checklist: build → local deploy → verify (am I done?)\n\nThe single \"am I done?\" gate for a full build-to-local-deploy task. Each phase\nlinks the detail above or the domain skill that owns it; walk it top to bottom\nand don't report success until every applicable box holds (skip a box only when\nthe project genuinely doesn't use that resource).\n\n**Build**\n- [ ] Non-deploying validation passed — typecheck / lint / unit tests, whatever the project has.\n- [ ] Model B only: `npm run build:functions` produced `.js` files under `volcano/functions/` (native JS skips this).\n\n**Local deploy** — run the \"Local deploy sequence\" above, in that order:\n- [ ] `volcano start` — local stack up and `volcano status` healthy.\n- [ ] `volcano variables deploy` — env vars synced (before functions).\n- [ ] `volcano functions deploy --all` — every function in `volcano/functions/` deployed.\n- [ ] `volcano config deploy` — declared config sections reconciled (after functions).\n- [ ] `volcano migrations deploy --all -d app` — migrations applied (schema ready).\n\n**Verify** — deploying is not the finish line (see \"Verify a local deploy\"):\n- [ ] Each function smoke-tested with `volcano functions invoke <name>` — asserted on status/body, not read off the deploy output.\n- [ ] Multi-user / RLS isolation confirmed with the SDK recipe if the app has per-user data.\n- [ ] Static-correctness items from \"Verification Checklist\" below hold (layout, one-statement migrations, `auth.uid()`, `VOLCANO_DATABASE`, no `src/api/`).\n\nThe per-domain skills carry their own \"Verification Checklist\" for domain-specific\nasserts — consult the ones the build touched: `volcano-functions`, `volcano-durable`,\n`volcano-auth`, `volcano-database`, `volcano-storage`, `volcano-realtime`.\n\nCloud is out of scope here — never auto-deploy to cloud (see `AGENTS.md` safety model).\n\n## Forbidden Patterns\n- Do NOT create an `src/api/index.ts` route dispatcher or `openapi.yaml` — Volcano Functions deploy individually from `volcano/functions/`, not through a single entry point.\n- Do NOT expect `VOLCANO_API_URL`, `VOLCANO_ANON_KEY`, or `VOLCANO_DATABASE` to be auto-injected — define them as project variables via `volcano variables deploy` (local) or `volcano cloud variables deploy` (cloud).\n- Do NOT use `VOLCANO_DB_NAME` — the canonical variable is `VOLCANO_DATABASE`.\n- Do NOT use bare `uid()`, `email()`, `role()` — always use the `auth.` schema prefix: `auth.uid()`, `auth.email()`, `auth.role()`.\n- Do NOT use `pg`/`pg-pool`/`DATABASE_URL` as a replacement for the SDK client — use `VOLCANO_DATABASE` and the SDK for all standard CRUD. Direct Postgres access is a discouraged, narrowly-scoped last resort ONLY for query-builder gaps that are provably impossible otherwise (joins/upserts/multi-statement transactions), NOT a general-purpose data layer or a reason to introduce an ORM as project architecture, and ONLY after rewriting `application_name` to `volcano_user_access:{user_id}` (raw `DATABASE_URL` bypasses RLS) — see `volcano-database`'s \"Direct Postgres Access\" section.\n- Do NOT use `jsonwebtoken` or `bcryptjs` directly — Volcano Auth handles tokens and password hashing.\n- Do NOT assume `__volcano_auth` is always present — it is injected only when the payload is an object and the request carries a valid token.\n- Do NOT expect `volcano functions deploy` to run your build — built `.js` files must exist under `volcano/functions/` on disk before deploy.\n- Do NOT put more than one SQL statement in a migration file, and do NOT wrap one in `BEGIN; ... COMMIT;` — `volcano migrations deploy` executes each file as a single statement and rejects multi-statement bodies (`ERROR: Multiple statements are not supported`). Split a multi-step schema change into several sequentially-numbered single-statement files instead.\n- Do NOT `require`/`import` an npm package (including `@volcano.dev/sdk`) from a native-JS (Model A) function without declaring it in a `package.json` — the packager never uploads `node_modules`, so an undeclared dependency fails at runtime with `Cannot find package`. Declare every imported package in a manifest so the runtime installs it.\n\n## Verification Checklist\n- Each deployed function was smoke-tested, not just deployed — `volcano functions invoke <name>` (runs as the local user) for a basic check, and the SDK recipe for multi-user/RLS isolation (see \"Verify a local deploy\").\n- Function handlers exist under `volcano/functions/` and each exports `handler`.\n- Shared code uses `_`-prefix directories (`_shared/`, `_lib/`).\n- `volcano/migrations/` contains `.sql` files with numeric-prefix alphabetical naming, each with exactly one SQL statement (no `BEGIN`/`COMMIT`, no multiple `;`-terminated statements in one file).\n- RLS policies use `auth.uid()` (with schema prefix), not bare `uid()`.\n- `volcano-config.yaml` (if present) has `version: 1`. There is no minimum\n  section requirement — a manifest can declare only `variables`, only\n  `project`, etc.; omitted sections are left untouched, not an error.\n- Environment variables are deployed via `volcano variables deploy` (local) or `volcano cloud variables deploy` (cloud) — not assumed auto-injected.\n- `VOLCANO_DATABASE` is used (not `VOLCANO_DB_NAME`).\n- If using the build model: `npm run build:functions` produces `.js` files under `volcano/functions/` before deploy.\n- Any npm package a native-JS function imports (e.g. `@volcano.dev/sdk`) is declared in a `volcano/functions/package.json` — an undeclared import fails at runtime with `Cannot find package`.\n- No `src/api/`, `openapi.yaml`, `dist/index.js`, or `dev-server.mjs` in the project.\n\n## Companion Skills\nThis skill defines the platform deploy contract. For domain-specific guidance:\n- `volcano-sdk` — top-level orientation and skill router.\n- `volcano-functions` — handler templates, invocation contract, user context, error handling.\n- `volcano-durable` — checkpointed handlers, replay rules, executions, and cloud CLI lifecycle.\n- `volcano-auth`, `volcano-database`, `volcano-storage`, `volcano-realtime` — per-domain APIs and patterns.\n- `volcano-nextjs` — Next.js frontend patterns (AuthProvider, middleware, server actions).\n- `volcano-uiux` — shared guidance for user-facing interfaces.\n- `volcano-typescript` — canonical TypeScript type definitions.\n- `volcano-error-handling` — reusable error-handling infrastructure.\n"
}

SHA-256 of public snapshot: 15457b84b504c745d286c2f7da6ab4f03e99bbeb6ccb99ccfb5c8774b2789d4f