{
  "name": "app-development",
  "title": "App Development - Functions, Database & API",
  "description": "Functions, database & API: write serverless functions, set up databases, deploy, test, and call via REST",
  "guid": "sk_plat_aapi",
  "category": "App development",
  "requiredTools": [
    "fn_manage",
    "db_manage",
    "db_sql",
    "project_deploy",
    "test_run"
  ],
  "content": "# App Development - Functions, Database & API\n\nEverything you need to build a backend on Gipity: serverless functions, databases, and REST APIs. This is the guide for the `web-fullstack` starter (frontend in `src/` + functions + a `migrations/` database) and the `api` starter (functions only, no frontend) - both install the structure below for you to fill in.\n\n## Workflow\n\nFor every new function:\n1. Write the function in `functions/{name}.js`\n2. Add it to `gipity.yaml` under `function_definitions` (name, auth, tables/fetch_domains)\n3. Write tests in `tests/api/{name}.test.js`\n4. Deploy: `project_deploy target=dev`\n5. Run tests: `test_run`\n\n**Strongly recommended: write a test file for every new function.** Use your judgment - skip tests only for trivial glue code (one-liners, obvious passthroughs), scratch/throwaway experiments, or when the user explicitly opts out. Default is: write tests. **Always deploy and test** after creating functions - don't ask permission, just do it.\n\n**In test files, do NOT `import` `node:test` or `node:assert`** - the harness provides `test()` and `assert` as globals. Writing `import { test } from 'node:test'` or `import assert from 'node:assert/strict'` will cause a duplicate-identifier SyntaxError.\n\n**After every deploy, read the phase results.** Any phase with `status: warning` starts with \"FIX\" and means something is deprecated or shaped wrong - usually in `gipity.yaml`. Treat these as TODOs, not info: fix the underlying file and redeploy. Don't leave deploy warnings unresolved. Similarly, `status: failed` phases block the deploy from completing correctly - fix before moving on.\n\n---\n\n## Writing Functions\n\nFunctions are JavaScript files in `functions/`. Each exports a default async handler:\n\n```js\n// functions/get-items.js\nexport default async function getItems(ctx, { db }) {\n    const { category } = ctx.body;\n    const { rows } = await db.query(\n        'SELECT * FROM items WHERE category = $1', [category]\n    );\n    return rows;\n}\n```\n\n**Arguments:**\n- First: `ctx` = request context with `{ body, query, headers, method, auth }`\n- Second: services object `{ db, fetch, secrets, env, console }`\n\n**Return value** becomes `{ data: <your return> }` in the HTTP response.\n\n### Request context (`ctx`)\n\n```js\nctx.body     // POST body (parsed JSON)\nctx.query    // URL query parameters\nctx.headers  // HTTP headers (safe subset - content-type, accept, user-agent, x-request-id, origin, referer, x-forwarded-for, x-real-ip)\n             // x-forwarded-for is the original caller's IP chain - use ctx.headers['x-forwarded-for']?.split(',')[0].trim() to get the user's IP,\n             // useful when calling /services/location/ip from a function (see app-location)\nctx.method   // HTTP method (always uppercase, e.g. \"POST\")\nctx.auth     // { userId, userGuid, displayName, role } - populated when auth_level is 'user' or 'member'\n```\n\n---\n\n## Database\n\n### Setup\n- `db_manage` - create or drop databases\n- `db_list` - list existing databases\n- `db_sql` - execute SQL (SELECT, INSERT, UPDATE, DELETE, CREATE TABLE, etc.)\n\nUse PostgreSQL syntax: `SERIAL`, `BOOLEAN`, `TIMESTAMPTZ`, `JSONB`, `TEXT`.\n\nLimits: database count is per-plan (run `credits_products` to see), 500 rows / 128 KB per query, 50,000 chars per statement.\n\nDestructive operations (DROP, TRUNCATE) are auto-confirmed by the platform - just call db_sql directly.\n\n### Database Helpers in Functions (via `db`)\n\n```js\n// Raw SQL with parameters\nconst { rows } = await db.query('SELECT * FROM orders WHERE user_id = $1', [userId]);\n\n// Convenience methods\nconst user  = await db.findOne('users', { id: userId });\nconst items = await db.findMany('orders', {\n  where: { status: 'pending' },\n  orderBy: 'created_at DESC',\n  limit: 10,\n  offset: 0,\n});\nconst inserted = await db.insert('orders', { user_id: 1, total: 99.99 });\nconst updated  = await db.update('orders', { id: orderId }, { status: 'shipped' });\nawait db.delete('orders', { id: orderId });\n```\n\n**Limits:** max_queries 50, max_rows_read 10,000, max_rows_affected 1,000.\n**Security:** Only declared tables are accessible. DDL (CREATE, ALTER, DROP) is blocked inside functions.\n\n---\n\n## External HTTP (via `fetch`)\n\n```js\nexport default async function getWeather(ctx, { fetch }) {\n    const { zip } = ctx.body;\n    const res = await fetch('https://api.example.com/weather?zip=' + zip);\n    return await res.json();\n}\n```\n\n**Limits:** max_fetches 10. 10s timeout. 1MB response limit.\n**Security:** Only declared `fetch_domains` are allowed. Private IPs blocked (SSRF protection).\n\n## Secrets (via `secrets`)\n\n```js\nconst apiKey = await secrets.get('STRIPE_KEY');\n```\n\nEncrypted per project. Manage via `fn_manage`.\n\n## Environment Variables (via `env`)\n\n```js\nconst mode = env.APP_MODE; // Read-only\n```\n\n## Logging (via `console`)\n\n```js\nconsole.log('Processing', { orderId });\nconsole.warn('Rate limit approaching');\nconsole.error('Failed', { error: err.message });\n```\n\nCaptured in execution logs (`fn_manage logs`).\n\n## Using auth\n\nUse `ctx.auth.userId` for user-scoped data (available when the function's `auth` is `user` or `member`):\n\n```js\nexport default async function myTodos(ctx, { db }) {\n    const { rows } = await db.query(\n        'SELECT * FROM todos WHERE user_id = $1',\n        [ctx.auth.userId]\n    );\n    return rows;\n}\n```\n\n---\n\n## gipity.yaml - function permissions\n\nFunctions auto-deploy as public endpoints, and the deploy pipeline auto-adds an entry to `gipity.yaml` for any undeclared function. For the full manifest format, phases, and deploy flags see [deploy](deploy.md); this section covers only `function_definitions` (per-function permissions).\n\nDeclare in `function_definitions` only when you need to:\n- Change auth level (`user`, `member`)\n- Grant database access (`tables`)\n- Allow external HTTP calls (`fetch_domains`) - **required** to call app-scoped services like llm/location/image/tts, since those live at `a.gipity.ai`\n- Declare intended services (`services: [...]`) - documentation-only; services are NOT injected as JS objects, always call them via `fetch` with an X-App-Token. See `app-llm` and `app-location` for the pattern.\n\n```yaml\n- name: functions\n  type: functions\n  source: functions\n  function_definitions:\n    - name: get-items\n      auth: public\n      tables: [items]           # DB access\n    - name: get-weather\n      auth: public\n      fetch_domains: [api.example.com]  # HTTP access\n    - name: my-todos\n      auth: user                # Requires login\n      tables: [todos]\n    - name: hello\n      auth: public              # Auto-added by deploy (no special permissions)\n```\n\n---\n\n## Writing Tests\n\nCreate a test file for each function:\n\n```js\n// tests/api/get-items.test.js\ntest('get-items returns items for a category', async (ctx) => {\n    const result = await ctx.fn.call('get-items', { category: 'fruit' });\n    assert.ok(Array.isArray(result.data), 'should return an array');\n});\n\ntest('get-items handles missing category', async (ctx) => {\n    const result = await ctx.fn.call('get-items', {});\n    assert.ok(result.data.error, 'should return an error');\n});\n```\n\nTest happy path AND edge cases. Run with `test_run`. Filter: `test_run filter_path=api/get-items`.\n\n---\n\n## Calling Functions\n\nPublic functions need no authentication:\n```bash\ncurl -s -X POST https://a.gipity.ai/api/<PROJECT_GUID>/fn/<name> \\\n  -H 'Content-Type: application/json' -d '{\"key\": \"value\"}'\n```\n\n**Always use the project GUID in URLs - never the slug.**\n\n## Auth Levels\n- **public** - no auth needed, call directly\n- **user** - requires X-App-Token, X-Api-Key, or Authorization header (*load `app-auth` for details*)\n- **member** - auth header + project membership\n\n## Versioning & Rollback\n\nEvery update creates an immutable version:\n```\nfn_manage rollback --name my-function --version 3\n```\n\n## Management Commands\n```\nfn_manage list                          # List all functions\nfn_manage logs --name X                 # Execution logs\nfn_manage delete --name X               # Delete\nfn_manage rollback --name X --version N # Rollback\n```\n\n## Rate Limits\n- 300 requests per 5-minute window (per IP)\n- 500 rows / 128 KB per query result\n\n---\n\n## Related Skills\n\nLoad these for specific features:\n- `web-app-basics` - HTML/CSS/JS patterns and templates\n- `deploy` - the deploy pipeline and `gipity.yaml` manifest\n- `app-debugging` - inspect deployed pages, screenshots, function logs\n- `app-auth` - user authentication (Sign in with Gipity)\n- `app-realtime` - real-time multiplayer rooms and WebSocket\n- `app-llm` - AI/LLM service for your app\n- `app-image` - image generation (OpenAI, BFL/Flux)\n- `app-video` - video generation and understanding\n- `app-audio` - sound effects, music, transcription\n- `app-tts` - text-to-speech (ElevenLabs, OpenAI)\n- `app-files` - file uploads (presigned S3, up to 30GB)\n- `app-location` - user location & geocoding for deployed apps\n- `3d-engine` - minimal 3D multiplayer template (Three.js + Rapier + Colyseus)\n- `3d-world` - playable 3D multiplayer starter on top of 3d-engine\n- `2d-game` - 2D games (Phaser 3)"
}
