{"service":"fullstacked-cloud-docs","version":"1.22.3","environment":"live","generatedAt":"2026-10-24T09:39:52.203Z","formats":{"html":"https://docs.fullstacked.cloud/","markdown":"https://docs.fullstacked.cloud/?format=md","json":"https://docs.fullstacked.cloud/?format=json"},"services":{"console":"https://console.fullstacked.cloud","auth":"https://auth.fullstacked.cloud","git":"https://git.fullstacked.cloud","tunnels":"https://tunnels.fullstacked.cloud","tunnelsHost":"tunnels.fullstacked.cloud","mcp":"https://mcp.fullstacked.cloud","install":"https://install.fullstacked.cloud","docs":"https://docs.fullstacked.cloud"},"runtime":{"version":"1.0.0-alpha.1844","guide":"https://docs.fullstacked.cloud/runtime?format=md"},"dataEngineLimits":{"postgres":{"image":"postgres:18.6-alpine","port":5432,"memoryMB":256,"cpus":2,"pids":256},"redis":{"image":"redis:8.10.2-alpine","port":6379,"memoryMB":256,"cpus":1,"pids":128},"mongodb":{"image":"mongo:8.3.11-noble","port":27017,"memoryMB":512,"cpus":2,"pids":512},"s3":{"image":"rustfs/rustfs:1.0.0","port":9000,"memoryMB":768,"cpus":2,"pids":1024}},"projectRuntimeEnv":[{"name":"TUNNELS_HOST","injectedBy":"GET /api/v1/projects/:id/env","description":"Tunnels hub host for tunnel.register from fullstacked/tunnel."},{"name":"AUTH_HOST","injectedBy":"GET /api/v1/projects/:id/env","description":"Bare host of the Authentication service."},{"name":"AUTH_URL","injectedBy":"GET /api/v1/projects/:id/env","description":"Base URL of the Authentication service (GET ${AUTH_URL}/api/v1/me)."},{"name":"DOCS_URL","injectedBy":"GET /api/v1/projects/:id/env","description":"Base URL of this documentation (?format=md | ?format=json)."},{"name":"AUTH_TOKEN","injectedBy":"Client GUI / action runner at launch (in memory, never stored)","description":"The signed-in user's fsc_* bearer token: Authorization: Bearer ${AUTH_TOKEN}."}],"topics":[{"slug":"actions","title":"Project Actions","url":"https://docs.fullstacked.cloud/actions","markdownUrl":"https://docs.fullstacked.cloud/actions?format=md","jsonUrl":"https://docs.fullstacked.cloud/actions?format=json","markdown":"# Project Actions\n\nAn action is a TypeScript file `actions/<name>.ts` in a project. Its default export is an ordinary async function: the app imports and calls it directly, and once a `v*` release tag is built, any authorized caller can run the same function remotely in a sandbox with `POST https://git.fullstacked.cloud/api/v1/projects/:id/actions/:name/run` (MCP: `projects_run_action`). Remote runs get the caller's environment and token, so data access follows the caller's permissions.\n\n## File contract\n\n```ts\n// actions/add-todo.ts\nimport type pg from \"pg\";\nimport { getPool } from \"../db\";\n\nexport const action = {\n    title: \"Add Todo\",\n    description: \"Creates a todo item\",\n    parameters: {\n        type: \"object\",\n        properties: {\n            title: { type: \"string\", minLength: 1, description: \"Todo title\" },\n            priority: { type: \"string\", enum: [\"low\", \"medium\", \"high\"], default: \"medium\" },\n        },\n        required: [\"title\"],\n        additionalProperties: false,\n    },\n};\n\ntype Params = { title: string; priority?: \"low\" | \"medium\" | \"high\" };\n\n/** Remote runs only: runs first, its return value is the handler's ctx. */\nexport async function setup(params: Params) {\n    return { pool: await getPool() };\n}\n\n/** The handler: called by the app directly (no ctx) and by remote runs. */\nexport default async function addTodo(params: Params, ctx?: { pool: pg.Pool }) {\n    const pool = ctx?.pool ?? (await getPool());\n    const { rows } = await pool.query(\n        \"INSERT INTO todos (title, priority) VALUES ($1, $2) RETURNING *\",\n        [params.title, params.priority ?? \"medium\"]\n    );\n    return rows[0];\n}\n\n/** Remote runs only: always runs after the handler, even when it threw. */\nexport async function teardown(ctx?: { pool: pg.Pool }) {\n    await ctx?.pool.end();\n}\n```\n\n| Export | Required | Rules |\n| :--- | :--- | :--- |\n| `export const action` | Yes (the release build fails without it) | A **static literal**: objects, arrays, strings (template strings without `${}`), numbers, booleans, `null`, comments, trailing commas; a type annotation, `as const` and `satisfies` are allowed. No identifiers, calls, spreads or shorthand properties. Fields: `title?` (defaults to the name), `description?`, `parameters?` (a JSON Schema whose root is `{ \"type\": \"object\" }`, default no properties), `name?` (must equal the file name). |\n| `export default` | Yes | `(params, ctx?) => result`. Receives validated params with schema defaults applied. The result must be JSON-serializable (`undefined` becomes `null`). |\n| `export function setup(params)` | No | Remote runs only. Opens resources; returns `ctx`. |\n| `export function teardown(ctx)` | No | Remote runs only. Always runs, bounded by 5 s (`ACTION_TEARDOWN_TIMEOUT_MS`); its errors are logged and never change the outcome. |\n\nFile rules:\n\n- Only top-level `actions/*.ts` files are actions. Files starting with `_` or `.`, `*.d.ts`, subdirectories and other extensions are helpers (`actions/_shared.ts`).\n- The file name is the action name: `^[a-z0-9][a-z0-9_-]{0,63}$`. Any other `actions/*.ts` name fails the build.\n- Never run the handler at module top level: remote runs import the module, so top-level side effects run too. Detect a remote run with `process.env.FULLSTACKED_ACTION_RUN === \"1\"` if needed.\n- Hooks are plain exports and can be shared: `export { setup, teardown } from \"./_lifecycle\";`.\n\n## Parameter schema\n\nSupported keywords: `type` (`string`, `number`, `integer`, `boolean`, `object`, `array`, `null`, or a list), `properties`, `required`, `additionalProperties`, `items`, `enum`, `const`, `default`, `minLength`, `maxLength`, `pattern`, `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `minItems`, `maxItems`. Other keywords are ignored. Invalid params answer `400 INVALID_PARAMS` naming the parameter, before anything runs. `required` is an array on the object schema, not a flag on each property.\n\n## Calling from the app\n\n```tsx\nimport addTodo from \"./actions/add-todo\";\n\nasync function onSubmit(title: string) {\n    const todo = await addTodo({ title });\n}\n```\n\nIn the app no schema validation or hooks run: the handler gets exactly what you pass, and resources live as long as the app.\n\n## Remote runs\n\nActions run only from a successfully built release: push a tag starting with `v` (MCP `projects_create_tag`), the build bundles each action with its dependencies, and an invalid action fails the build.\n\n```http\nPOST https://git.fullstacked.cloud/api/v1/projects/<projectId>/actions/add-todo/run\nAuthorization: Bearer <fsc_* token>\nContent-Type: application/json\n\n{ \"tag\": \"v1.2.0\", \"params\": { \"title\": \"Ship docs\" } }\n```\n\n`tag` is optional: the newest succeeded release the caller can access is used. The response is `200` even when the run failed or timed out:\n\n```json\n{\n  \"run_id\": \"3c1f0e2a-...\",\n  \"action\": \"add-todo\",\n  \"tag\": \"v1.2.0\",\n  \"status\": \"succeeded\",\n  \"exit_code\": 0,\n  \"error\": null,\n  \"duration_ms\": 1840,\n  \"created_at\": \"2026-10-23T14:02:11.000Z\",\n  \"output\": { \"id\": 42, \"title\": \"Ship docs\", \"priority\": \"medium\" }\n}\n```\n\n| Status | When |\n| :--- | :--- |\n| `succeeded` | The handler returned; `output` is its JSON result. |\n| `failed` | `setup` or the handler threw (`error` is its message; an error with a 5-character `code` such as a PostgreSQL SQLSTATE becomes `SQLSTATE <code>: <message>`), a non-zero exit, or no valid result. |\n| `timeout` | The run exceeded 30 s (`ACTION_TIMEOUT_MS`) and was killed. |\n\nOther endpoints (same host, `Authorization: Bearer`):\n\n| Endpoint | Returns |\n| :--- | :--- |\n| `GET /api/v1/projects/:id/actions?tag=` | `{ tag, actions: [{ name, title, description, file, bundle, parameters }] }` of a release (MCP `projects_list_actions`). |\n| `GET /api/v1/projects/:id/actions/runs?tag=&action=&status=&limit=` | `{ runs: [{ run_id, action, tag, caller, caller_name, status, exit_code, error, duration_ms, created_at }] }`, newest first (`limit` 1 to 200, default 50). Users without read access see only their own runs. |\n| `GET /api/v1/projects/:id/actions/runs/:runId` | `{ run }`: the same fields plus `output`, `logs` (last 256 KiB of stdout and stderr) and `finished_at`. |\n\nErrors: `400 INVALID_PARAMS` params rejected by the schema, `403` tag not accessible, `404` unknown action, tag or release without a successful build, `502` environment resolution failed, `503` sandbox unavailable.\n\n## Sandbox environment\n\nEach run is a fresh container running the bundled action with `fullstacked --file` (runtime 1.0.0-alpha.1844). The code runs under Node: `process` is Node's own, there is no `document`, `fullstacked/auth` (which opens a window) cannot be used, and a relative `fetch(\"/...\")` fails. Keep UI code out of the modules an action imports.\n\n| Limit | Value |\n| :--- | :--- |\n| Timeout | 30 s by default |\n| Memory | 512 MiB, no swap |\n| CPU / processes | 1 vCPU, 256 processes |\n| Network | Internet allowed (including tunnels.fullstacked.cloud, so managed data and Edge tunnels work through `fullstacked/tunnel`); private networks and the host are blocked |\n| Packages | Bundled at release time; nothing is installed at run time |\n\n`process.env` holds the project environment resolved **for the caller** ([Injected process.env](https://docs.fullstacked.cloud/project-runtime)), so a read-only caller gets read-only database credentials and writes fail. On top of it:\n\n| Variable | Value |\n| :--- | :--- |\n| `AUTH_TOKEN` | The caller's own `fsc_*` token (calls to `/api/v1/me` return the caller). |\n| `ACTION_NAME`, `ACTION_RUN_ID`, `ACTION_PARAMS` | Name, run id, validated params as JSON. |\n| `RELEASE_TAG`, `PROJECT_ID` | The release and project being run. |\n| `FULLSTACKED_ACTION_RUN` | `\"1\"`. |\n| `ACTION_TEARDOWN_TIMEOUT_MS` | Teardown bound in ms. |\n\nThese run variables override custom project variables of the same name. Output written to `console.log` / `console.error` is kept as logs; only the handler's return value is the result. Open pools or sockets cannot keep a run alive: the runner exits after `teardown`.\n\n## Who can run\n\nAnyone with access to the release tag (owner, any project tier, or a release-only grant) can list and run actions; the run executes with that caller's permissions on linked data and tunnels.\n\n## Endpoints\n\n### GET /api/v1/projects/:id/actions\n\nActions compiled in a release (?tag=, default the newest release the caller can access).\n\n- **Service**: `git` (`https://git.fullstacked.cloud`)\n- **Auth**: Bearer fsc_*\n\n### POST /api/v1/projects/:id/actions/:name/run\n\nRuns a release's action with the caller's own token and data wiring; answers 200 with the run result even when the run failed or timed out.\n\n- **Service**: `git` (`https://git.fullstacked.cloud`)\n- **Auth**: Bearer fsc_*\n\nRequest:\n\n```json\n{\n  \"tag\": \"v1.0.0\",\n  \"params\": {\n    \"customerId\": \"cus_123\"\n  }\n}\n```\n\ncurl:\n\n```bash\ncurl -sS -X POST -H \"Authorization: Bearer $AUTH_TOKEN\" -H \"Content-Type: application/json\" -d '{\"tag\":\"v1.0.0\",\"params\":{\"customerId\":\"cus_123\"}}' \"https://git.fullstacked.cloud/api/v1/projects/<projectId>/actions/sync-orders/run\"\n```\n\n### GET /api/v1/projects/:id/actions/runs\n\nPast action runs, filtered by ?tag=, ?action=, ?status= and ?limit=.\n\n- **Service**: `git` (`https://git.fullstacked.cloud`)\n- **Auth**: Bearer fsc_*\n\n### GET /api/v1/projects/:id/actions/runs/:runId\n\nOne action run.\n\n- **Service**: `git` (`https://git.fullstacked.cloud`)\n- **Auth**: Bearer fsc_*\n"}],"endpoints":[{"service":"git","topic":"actions","auth":"Bearer fsc_*","id":"git.listActions","method":"GET","path":"/api/v1/projects/:id/actions","summary":"Actions compiled in a release (?tag=, default the newest release the caller can access).","fullUrl":"https://git.fullstacked.cloud/api/v1/projects/:id/actions"},{"service":"git","topic":"actions","auth":"Bearer fsc_*","id":"git.runAction","method":"POST","path":"/api/v1/projects/:id/actions/:name/run","summary":"Runs a release's action with the caller's own token and data wiring; answers 200 with the run result even when the run failed or timed out.","requestExample":{"tag":"v1.0.0","params":{"customerId":"cus_123"}},"codeExamples":{"curl":"curl -sS -X POST -H \"Authorization: Bearer $AUTH_TOKEN\" -H \"Content-Type: application/json\" -d '{\"tag\":\"v1.0.0\",\"params\":{\"customerId\":\"cus_123\"}}' \"https://git.fullstacked.cloud/api/v1/projects/<projectId>/actions/sync-orders/run\""},"fullUrl":"https://git.fullstacked.cloud/api/v1/projects/:id/actions/:name/run"},{"service":"git","topic":"actions","auth":"Bearer fsc_*","id":"git.listActionRuns","method":"GET","path":"/api/v1/projects/:id/actions/runs","summary":"Past action runs, filtered by ?tag=, ?action=, ?status= and ?limit=.","fullUrl":"https://git.fullstacked.cloud/api/v1/projects/:id/actions/runs"},{"service":"git","topic":"actions","auth":"Bearer fsc_*","id":"git.getActionRun","method":"GET","path":"/api/v1/projects/:id/actions/runs/:runId","summary":"One action run.","fullUrl":"https://git.fullstacked.cloud/api/v1/projects/:id/actions/runs/:runId"}]}