# App Development Reference - Module Rules, Multi-file Functions & Limits

The long tail of [app-development](app-development.md) - load that skill first for the core workflow (write a function, wire `gipity.yaml`, deploy, test). This reference covers the function runtime's module system in depth, plus operational details you reach for on demand.

## ES module syntax - what you can write

The runtime compiles your code as a real ES module (since 2026-05-29). Any valid single-file ESM parses cleanly:

```js
// All of this works in one file:
import { z } from './schemas.js';           // relative imports (multi-file only - see below)
export const VERSION = 3;                   // top-level const, exported or not
export function validate(input) { ... }     // named exports alongside the default
const cfg = await loadConfig();             // top-level await
export default async function handler(ctx, { db }) { ... }
```

What does NOT work and the exact error you'll see:
- **Bare imports** (`import _ from 'lodash'`) → *"Bare import 'lodash' is not allowed in function code. Use a relative path like './helpers.js'."* No `node_modules`, no built-in Node modules (`fs`, `path`, `crypto` - for secrets use the injected `randomToken(chars)` service).
- **No default export** → *"Function must \`export default\` a callable handler."*
- **Default export isn't a function** → *"Function 'export default' must be a function (got object)."*
- **Genuine parse error** → *"Function file 'index.js' did not parse: <v8 message>"* - the file name in the message tells you which file the error is in (helpful in multi-file functions).

## Multi-file functions

When a function grows past a single file - testable helpers, shared constants, etc. - turn it into a **directory**:

```
functions/
  song-render/
    index.js          # entry - must have `export default async function ...`
    sanitize.js       # helper - `export function sanitizeRenderOptions(...)`
    constants.js      # `export const DEFAULT_MODE = 'preview'`
```

`index.js` is the entry; sibling files are reached via relative `import`:

```js
// functions/song-render/index.js
import { sanitizeRenderOptions } from './sanitize.js';

export default async function songRender(ctx, { db, jobs, guid }) {
  const opts = sanitizeRenderOptions(ctx.body?.render_options);
  // ...
}
```

**Why use multi-file:**
- **Testable helpers.** Unit-test directly from `tests/*.test.js` with `import { x } from '../functions/song-render/sanitize.js'`. Single-file helpers can only be exercised through end-to-end function calls.
- **Cleaner separation.** Pure helpers stay pure.

**Helpers don't see services.** Only the entry module receives `ctx`, `db`, `fetch`, `secrets`, `env`, `console`, `jobs`, `guid`, and the app services (`llm`, `image`, `tts`, `sound`, `music`, `video`, `transcribe`, `location`). Inside a helper, those names are `undefined`. If a helper needs the DB, the entry passes it as an argument:

```js
// functions/song-render/sanitize.js - PURE helper, no service access
export function sanitizeRenderOptions(raw) { /* validates raw, returns clean obj */ }

// functions/song-render/persist.js - needs db, so accept it as arg
export async function saveRender(db, songGuid, opts) {
  await db.query('INSERT INTO renders ...', [songGuid, opts]);
}

// functions/song-render/index.js - entry passes db down
import { sanitizeRenderOptions } from './sanitize.js';
import { saveRender } from './persist.js';
export default async function (ctx, { db }) {
  const opts = sanitizeRenderOptions(ctx.body);
  await saveRender(db, ctx.body.song_guid, opts);
}
```

**Import rules and errors:**
- Imports must be relative - `./helper.js` or `./utils/foo.js` (bare specifiers rejected as above)
- Imports can't escape the function directory - `../../etc/foo.js` fails with *"Import '../../etc/foo.js' escapes the function directory."* The one sanctioned exception is the shared `../_lib/` prefix (next section).
- A missing sibling fails clearly: *"Cannot resolve import './nope.js' → 'nope.js' - file not in function payload. Available: index.js, sanitize.js"*
- Circular imports work (a → b → a) - ESM cycles resolve via the module loader's link cache
- The entry filename is always `index.js`

**Limits:** up to 20 files per function (shared `_lib` files count toward this), 256 KB total source, max 4 levels of nesting.

Pick the shape that fits: single `functions/foo.js` for simple handlers; `functions/foo/` directory once you have a helper you'd like to unit-test.

## Shared modules across functions (`functions/_lib/`)

When two or more functions need the same code - a schema registry, validation, SQL builders - put it in `functions/_lib/` instead of copy-pasting it into each function directory (copies drift):

```
functions/
  _lib/
    registry.js       # shared - `export function describeObject(...)`
  record-read/
    index.js          # imports it: `import { describeObject } from '../_lib/registry.js'`
  record-write.js     # single-file functions can import it the same way
```

At deploy, `_lib/` files are bundled into **every** function's payload, so editing a shared module re-deploys all functions automatically. The rules:

- Import as `../_lib/<file>.js` from any function file (works from single-file functions too)
- `_lib` files may import each other (`./fmt.js`) but can't import function files - shared code stays function-agnostic
- `_lib` files are pure like helpers: no `db`/`ctx`/services; the entry passes them as arguments
- `_lib` is reserved: a function named `_lib` or a `_lib/` subdirectory inside a function directory is a deploy error
- Unit-test shared modules from `tests/*.test.js` via `import { x } from '../functions/_lib/registry.js'`

The function runtime supports single-file ESM (shipped 2026-05-29), multi-file functions with relative `./helper.js` imports (2026-05-30), and shared `_lib` modules imported as `../_lib/<file>.js` (2026-06-09).

## Kits that ship backend pieces (`gipity add records` …)

Some kits install more than frontend code. A kit whose manifest declares
`functions: true` / `migrations: true` copies its `functions/` and
`migrations/` trees into the app's own `functions/` and `migrations/` (the
`records` and `agent-api` kits are the prototypes - generic registry-driven
CRUD with an audit event spine, and named API keys for agent writes). Rules:

- **Kit-owned files are sealed.** Functions, `_lib` modules, and migrations a
  kit installs are upgraded by re-running `gipity add <kit>` at a newer
  version, which overwrites them. Never edit them in place; put app code in
  your own function/migration files.
- Kit migrations order themselves by filename (`000-kit-*.sql` sorts before
  your `001-*.sql`). If the app has no `sql` deploy phase, the install adds one.
- The kit's `gipity.yaml` function entries declare only the kit's own core
  tables. **When you register your own objects/tables, append them to the
  kit functions' `tables:` lists yourself** - e.g. the records kit's
  `record-read`/`record-write` need your object tables listed before they can
  serve them. Your edits survive re-install (entries are merged by name, never
  clobbered).
- Kits can require other kits (`requires: ["records"]`) - `add` errors with
  the missing prerequisite's name.

Full guide to the `records` + `views` kits (registry-driven CRUD, field
types, event spine, generated tables/kanban/forms) → the
[app-records](app-records.md) skill.

## 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) — **waived during `gipity test`** (all test runs share one sandbox IP, so the platform exempts a project's own test run; see "What else is isolated" above)
- 500 rows / 128 KB per query result

---

## Related Skills

- `app-development` - the core guide: function shape, `ctx`, `gipity.yaml` permissions, and the deploy/test loop
- `app-database` - the per-app Postgres database: migrations, the `db` helper, transactions, table permissions
- `app-testing` - testing deployed functions: `ctx.fn.call` / `callAs`, the isolated test DB, fixtures
- `app-records` - the records + views kits: registry-driven CRUD, field types, event spine
- `deploy` - the deploy pipeline and `gipity.yaml` manifest

