# App Development - Functions, Database & API

Everything 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.

## Workflow

For every new function:
1. Write the function in `functions/{name}.js`
2. Add it to `gipity.yaml` under `function_definitions` (name, auth, tables/fetch_domains)
3. Write tests in `tests/api/{name}.test.js`
4. Deploy: `project_deploy target=dev`
5. Run tests: `test_run`

**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.

**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.

**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.

---

## Writing Functions

Functions are JavaScript files in `functions/`. Each exports a default async handler:

```js
// functions/get-items.js
export default async function getItems(ctx, { db }) {
    const { category } = ctx.body;
    const { rows } = await db.query(
        'SELECT * FROM items WHERE category = $1', [category]
    );
    return rows;
}
```

**Arguments:**
- First: `ctx` = request context with `{ body, query, headers, method, auth }`
- Second: services object `{ db, fetch, secrets, env, console }`

**Return value** becomes `{ data: <your return> }` in the HTTP response.

### Request context (`ctx`)

```js
ctx.body     // POST body (parsed JSON)
ctx.query    // URL query parameters
ctx.headers  // HTTP headers (safe subset - content-type, accept, user-agent, x-request-id, origin, referer, x-forwarded-for, x-real-ip)
             // 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,
             // useful when calling /services/location/ip from a function (see app-location)
ctx.method   // HTTP method (always uppercase, e.g. "POST")
ctx.auth     // { userId, userGuid, displayName, role } - populated when auth_level is 'user' or 'member'
```

---

## Database

### Setup
- `db_manage` - create or drop databases
- `db_list` - list existing databases
- `db_sql` - execute SQL (SELECT, INSERT, UPDATE, DELETE, CREATE TABLE, etc.)

Use PostgreSQL syntax: `SERIAL`, `BOOLEAN`, `TIMESTAMPTZ`, `JSONB`, `TEXT`.

Limits: database count is per-plan (run `credits_products` to see), 500 rows / 128 KB per query, 50,000 chars per statement.

Destructive operations (DROP, TRUNCATE) are auto-confirmed by the platform - just call db_sql directly.

### Database Helpers in Functions (via `db`)

```js
// Raw SQL with parameters
const { rows } = await db.query('SELECT * FROM orders WHERE user_id = $1', [userId]);

// Convenience methods
const user  = await db.findOne('users', { id: userId });
const items = await db.findMany('orders', {
  where: { status: 'pending' },
  orderBy: 'created_at DESC',
  limit: 10,
  offset: 0,
});
const inserted = await db.insert('orders', { user_id: 1, total: 99.99 });
const updated  = await db.update('orders', { id: orderId }, { status: 'shipped' });
await db.delete('orders', { id: orderId });
```

**Limits:** max_queries 50, max_rows_read 10,000, max_rows_affected 1,000.
**Security:** Only declared tables are accessible. DDL (CREATE, ALTER, DROP) is blocked inside functions.

---

## External HTTP (via `fetch`)

```js
export default async function getWeather(ctx, { fetch }) {
    const { zip } = ctx.body;
    const res = await fetch('https://api.example.com/weather?zip=' + zip);
    return await res.json();
}
```

**Limits:** max_fetches 10. 10s timeout. 1MB response limit.
**Security:** Only declared `fetch_domains` are allowed. Private IPs blocked (SSRF protection).

## Secrets (via `secrets`)

```js
const apiKey = await secrets.get('STRIPE_KEY');
```

Encrypted per project. Manage via `fn_manage`.

## Environment Variables (via `env`)

```js
const mode = env.APP_MODE; // Read-only
```

## Logging (via `console`)

```js
console.log('Processing', { orderId });
console.warn('Rate limit approaching');
console.error('Failed', { error: err.message });
```

Captured in execution logs (`fn_manage logs`).

## Using auth

Use `ctx.auth.userId` for user-scoped data (available when the function's `auth` is `user` or `member`):

```js
export default async function myTodos(ctx, { db }) {
    const { rows } = await db.query(
        'SELECT * FROM todos WHERE user_id = $1',
        [ctx.auth.userId]
    );
    return rows;
}
```

---

## gipity.yaml - function permissions

Functions 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).

Declare in `function_definitions` only when you need to:
- Change auth level (`user`, `member`)
- Grant database access (`tables`)
- Allow external HTTP calls (`fetch_domains`) - **required** to call app-scoped services like llm/location/image/tts, since those live at `a.gipity.ai`
- 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.

```yaml
- name: functions
  type: functions
  source: functions
  function_definitions:
    - name: get-items
      auth: public
      tables: [items]           # DB access
    - name: get-weather
      auth: public
      fetch_domains: [api.example.com]  # HTTP access
    - name: my-todos
      auth: user                # Requires login
      tables: [todos]
    - name: hello
      auth: public              # Auto-added by deploy (no special permissions)
```

---

## Writing Tests

Create a test file for each function:

```js
// tests/api/get-items.test.js
test('get-items returns items for a category', async (ctx) => {
    const result = await ctx.fn.call('get-items', { category: 'fruit' });
    assert.ok(Array.isArray(result.data), 'should return an array');
});

test('get-items handles missing category', async (ctx) => {
    const result = await ctx.fn.call('get-items', {});
    assert.ok(result.data.error, 'should return an error');
});
```

Test happy path AND edge cases. Run with `test_run`. Filter: `test_run filter_path=api/get-items`.

---

## Calling Functions

Public functions need no authentication:
```bash
curl -s -X POST https://a.gipity.ai/api/<PROJECT_GUID>/fn/<name> \
  -H 'Content-Type: application/json' -d '{"key": "value"}'
```

**Always use the project GUID in URLs - never the slug.**

## Auth Levels
- **public** - no auth needed, call directly
- **user** - requires X-App-Token, X-Api-Key, or Authorization header (*load `app-auth` for details*)
- **member** - auth header + project membership

## Versioning & Rollback

Every update creates an immutable version:
```
fn_manage rollback --name my-function --version 3
```

## Management Commands
```
fn_manage list                          # List all functions
fn_manage logs --name X                 # Execution logs
fn_manage delete --name X               # Delete
fn_manage rollback --name X --version N # Rollback
```

## Rate Limits
- 300 requests per 5-minute window (per IP)
- 500 rows / 128 KB per query result

---

## Related Skills

Load these for specific features:
- `web-app-basics` - HTML/CSS/JS patterns and templates
- `deploy` - the deploy pipeline and `gipity.yaml` manifest
- `app-debugging` - inspect deployed pages, screenshots, function logs
- `app-auth` - user authentication (Sign in with Gipity)
- `app-realtime` - real-time multiplayer rooms and WebSocket
- `app-llm` - AI/LLM service for your app
- `app-image` - image generation (OpenAI, BFL/Flux)
- `app-video` - video generation and understanding
- `app-audio` - sound effects, music, transcription
- `app-tts` - text-to-speech (ElevenLabs, OpenAI)
- `app-files` - file uploads (presigned S3, up to 30GB)
- `app-location` - user location & geocoding for deployed apps
- `3d-engine` - minimal 3D multiplayer template (Three.js + Rapier + Colyseus)
- `3d-world` - playable 3D multiplayer starter on top of 3d-engine
- `2d-game` - 2D games (Phaser 3)
