{"id":48475,"plugin_id":"plugins_6ac57dac927c81919e4e14ef134d04e2","kind":"skill","collection_source":"plugin_package","comparison_source":null,"observed_at":"2026-10-24T13:36:32.012Z","digest":"2d8c864c29558c7480e2a37de6c0b905bd9709ce86d4639aeac693002567a491","against":null,"payload":{"description":"Use for Volcano server-side Functions and privileged or secret-bearing logic, including function invocation, generators such as QR codes or PDFs, outbound third-party APIs, orchestration, scheduled processing, and file or image processing.","included_files":[],"name":"volcano-functions","skill_md_contents":"---\nname: volcano-functions\ndescription: Use for Volcano server-side Functions and privileged or secret-bearing logic, including function invocation, generators such as QR codes or PDFs, outbound third-party APIs, orchestration, scheduled processing, and file or image processing.\n---\n# Volcano Functions Skill\n\n## Role\nUse Volcano Functions for privileged, secret-bearing, or orchestration-heavy backend logic. This skill is self-contained: invocation, response shape, handler templates, env, and error handling are embedded.\n\n## Workflow\n1. Confirm the operation belongs in Functions (privileged/secrets/heavy orchestration). If it's a plain RLS-protected query, do it client-side via Volcano Database instead.\n2. Implement invocation with explicit success and failure handling.\n3. Enforce user-context assumptions where authorization matters.\n4. Route persistent data access through the Volcano Database query-builder pattern. Only when a specific query-builder gap (joins/upserts/multi-statement transactions) is provably impossible otherwise, fall back to direct Postgres access as a discouraged, narrowly-scoped last resort (see `volcano-database`'s \"Direct Postgres Access\" section — requires the `application_name=volcano_user_access:{user_id}` rewrite to stay RLS-safe).\n5. Validate the client-vs-function boundary.\n\n## Invocation Contract\nEvery invocation returns `{ data, status, headers, version, error }`.\n- `status` — HTTP status from the function response.\n- `headers` — response headers.\n- `version` — value of `X-Volcano-Version` (`<version>` in production, `<env>-<version>` otherwise).\n- `error` — present on transport/runtime failure or non-2xx status; check this before consuming `data`.\n\n### Basic\n```ts\nconst { data, status, version, error } = await volcano.functions.invoke('send-welcome-email', {\n  template: 'welcome',\n  recipientId: user.id,\n});\nif (error) {\n  console.error('Function failed:', error.message);\n  return;\n}\n```\n\n### Typed payload + response\n```ts\ninterface DashboardStats { totalUsers: number; activeToday: number; revenue: number; }\nconst { data, status, headers, version, error } = await volcano.functions.invoke<\n  { timeframe: string },\n  DashboardStats\n>('get-dashboard-stats', { timeframe: 'last-30-days' });\n```\n\n### No payload\n```ts\nconst { data, status, version, error } = await volcano.functions.invoke('health-check');\n```\n\n## User Context\nFunctions automatically receive the caller's identity in `event.__volcano_auth`:\n- `auth.user_id` — the authenticated user's id.\n- `auth.email` — the user's email.\n- `auth.role` — the user's role (if set).\n- `auth.project_id` — the project this user is acting in.\n- `auth.access_token` — server-injected bearer token; use to call other Volcano APIs on the user's behalf.\n\nIf `__volcano_auth` is absent, the request is unauthenticated.\n\n## Handler Templates\n\nVolcano Functions return a standard response shape: handlers return `{ statusCode, body, headers? }` where `body` is a string. Use `JSON.stringify(...)` to encode JSON responses. **Use `statusCode: 200` for all successful responses (not `201`/`202`) — `volcano functions invoke` treats any non-200 status as a failed invocation, so a `201 Created` reads as a failure to the CLI and to callers checking status; put the created resource in the `body` with `200`.**\n\n### Basic handler\n```js\n// 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### Authenticated handler with Volcano Database\n```ts\n// functions/get-my-posts.ts\nimport { VolcanoAuth } from '@volcano.dev/sdk';\n\nfunction createClient(auth?: { access_token?: string }): VolcanoAuth {\n  const volcano = new VolcanoAuth({\n    apiUrl: process.env.VOLCANO_API_URL!,\n    anonKey: process.env.VOLCANO_ANON_KEY!,\n    accessToken: auth?.access_token,\n  });\n  volcano.database(process.env.VOLCANO_DATABASE!);\n  return volcano;\n}\n\nexport const handler = async (event: { __volcano_auth?: { access_token?: string } }) => {\n  const auth = event.__volcano_auth;\n  if (!auth) {\n    return { statusCode: 401, body: JSON.stringify({ error: 'Unauthorized' }) };\n  }\n\n  const volcano = createClient(auth);\n  const { data, error } = await volcano\n    .from('posts')\n    .select('id, title, created_at')\n    .order('created_at', { ascending: false });\n\n  if (error) {\n    return { statusCode: 500, body: JSON.stringify({ error: error.message }) };\n  }\n  return { statusCode: 200, body: JSON.stringify({ posts: data ?? [] }) };\n};\n```\n\n### Server-side mutation\n```ts\n// functions/publish-post.ts\nexport const handler = async (event: {\n  postId?: string;\n  __volcano_auth?: { access_token?: string };\n}) => {\n  const auth = event.__volcano_auth;\n  if (!auth) return { statusCode: 401, body: JSON.stringify({ error: 'Unauthorized' }) };\n  if (!event.postId) return { statusCode: 400, body: JSON.stringify({ error: 'postId is required' }) };\n\n  const volcano = createClient(auth);\n  const { data, error } = await volcano\n    .update('posts', { status: 'published' })\n    .eq('id', event.postId);\n\n  if (error) return { statusCode: 500, body: JSON.stringify({ error: error.message }) };\n  return { statusCode: 200, body: JSON.stringify({ post: data?.[0] ?? null }) };\n};\n```\n\n### Calling external APIs with secrets\n```js\n// functions/send-slack-notification.js\nexports.handler = async (event) => {\n  const auth = event.__volcano_auth;\n  if (!auth) return { statusCode: 401, body: JSON.stringify({ error: 'Unauthorized' }) };\n\n  const { channel, message } = event;\n  const webhookUrl = process.env.SLACK_WEBHOOK_URL;\n\n  const response = await fetch(webhookUrl, {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify({ channel, text: message, username: 'Volcano Bot' }),\n  });\n\n  if (!response.ok) {\n    return { statusCode: 500, body: JSON.stringify({ error: 'Failed to send notification' }) };\n  }\n  return { statusCode: 200, body: JSON.stringify({ success: true }) };\n};\n```\n\n## When to Use Functions\n| Use case | Example |\n|---|---|\n| Third-party APIs with secrets | Stripe, SendGrid, Slack |\n| Background / scheduled jobs | Daily aggregations, report generation |\n| Admin / privileged operations | Bulk moderation, approvals |\n| File processing | Image resize, PDF generation |\n| Multi-step orchestration | Stitching several API calls atomically |\n\nIf the work is \"fetch this user's rows\" with RLS, do it client-side; functions add latency and complexity for nothing.\n\n## Error Handling\n\n### Client side\n```ts\nconst { data, error } = await volcano.functions.invoke('process-payment', {\n  amount: 1999,\n  currency: 'usd',\n});\n\nif (error) {\n  // Network error or function threw\n  showErrorToast('Payment failed. Please try again.');\n  return;\n}\n\n// Business-logic errors are returned in the body, not as `error`\nif (data.error) {\n  showErrorToast(data.error);\n  return;\n}\n```\n\n### Function side\n```js\nexports.handler = async (event) => {\n  try {\n    const result = await processPayment(event);\n    return { statusCode: 200, body: JSON.stringify({ success: true, paymentId: result.id }) };\n  } catch (err) {\n    console.error('Payment error:', err);\n    return {\n      statusCode: 400,\n      body: JSON.stringify({ error: err.message, code: err.code || 'PAYMENT_FAILED' }),\n    };\n  }\n};\n```\n\n## Environment Variables\nFunctions receive **only user-defined project variables**. Volcano does not auto-inject any variables — you must deploy them yourself:\n```sh\nvolcano variables deploy           # local\nvolcano cloud variables deploy     # cloud (requires volcano login + volcano use)\n```\nThis reads from `volcano/volcano.env` and sets project-scoped variables available to all functions at runtime.\n\n**Canonical names** (the shared client factory expects these):\n- `VOLCANO_API_URL`, `VOLCANO_ANON_KEY`, `VOLCANO_DATABASE` (defaults to `'app'`).\n\n**Custom secrets** (any name): `STRIPE_SECRET_KEY`, `SENDGRID_API_KEY`, etc.\n\nNever hardcode secrets in code.\n\n## Best Practices\n- **Validate input** at the top of the handler; return `400` with a descriptive message.\n- **Check `__volcano_auth`** and return `401` early for unauthenticated calls.\n- **Keep clients request-scoped** — build the Volcano client inside the handler with the request's auth, not in module globals.\n- **Time-box long ops** — handlers have execution limits; abort or stream early.\n- **Use `console.log`** for debug output, then fetch it with the CLI — `volcano functions logs <name> --type runtime` (`--type build` for deploy/packaging errors) — before reaching for the Volcano dashboard, which is a secondary view for cloud projects only. `--follow` streams indefinitely like `tail -f`; bound it (e.g. `timeout 15 ... --follow`) rather than running it as a bare synchronous call.\n\n## Verification Checklist\n- Secret-bearing logic remains server-side.\n- Invocation handles `status` and `error` explicitly.\n- User context assumptions are explicit (`event.__volcano_auth`).\n- Database work inside functions uses the same Volcano Database query-builder flow as the rest of the app.\n- Input is validated; auth check fires before any side effects.\n\n## Optional Fallback References\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}