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:
- Write the function in
functions/{name}.js - Add it to
gipity.yamlunderfunction_definitions(name, auth, tables/fetch_domains) - Write tests in
tests/api/{name}.test.js - Deploy:
project_deploy target=dev - 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:
- 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)
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 databasesdb_list- list existing databasesdb_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)
// 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:
- 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 ata.gipity.ai - Declare intended services (
services: [...]) - documentation-only; services are NOT injected as JS objects, always call them viafetchwith an X-App-Token. Seeapp-llmandapp-locationfor the pattern.
- 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
- public - no auth needed, call directly
- user - requires X-App-Token, X-Api-Key, or Authorization header (load
app-authfor 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 templatesdeploy- the deploy pipeline andgipity.yamlmanifestapp-debugging- inspect deployed pages, screenshots, function logsapp-auth- user authentication (Sign in with Gipity)app-realtime- real-time multiplayer rooms and WebSocketapp-llm- AI/LLM service for your appapp-image- image generation (OpenAI, BFL/Flux)app-video- video generation and understandingapp-audio- sound effects, music, transcriptionapp-tts- text-to-speech (ElevenLabs, OpenAI)app-files- file uploads (presigned S3, up to 30GB)app-location- user location & geocoding for deployed apps3d-engine- minimal 3D multiplayer template (Three.js + Rapier + Colyseus)3d-world- playable 3D multiplayer starter on top of 3d-engine2d-game- 2D games (Phaser 3)