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:

// 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:

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

Request context (ctx)

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

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)

// 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)

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)

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

Encrypted per project. Manage via fn_manage.

Environment Variables (via env)

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

Logging (via console)

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):

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; this section covers only function_definitions (per-function permissions).

Declare in function_definitions only when you need to:

- 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:

// 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:

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

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


Related Skills

Load these for specific features: