A deployed function gets its capabilities on the second argument, (ctx, services). This skill covers the ones that reach past the app's own database: outbound HTTP, the platform's app services, secrets, env vars and logging. For the function shape, ctx, gipity.yaml permissions and the deploy/test loop, load app-development first; the db helper is in app-database and the storage helper in app-files-storage.

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 - watch this with real third-party APIs; a "give me everything" / ?detail=full query easily blows the cap and the call throws. Ask for the narrowest response (paginate, pick fields, a smaller mode=) rather than fetching big and trimming. To keep a file (an image, audio, a PDF a provider hands you a URL for), don't fetch it: await storage.importUrl(url) downloads it server-side, past the 1MB cap, straight into the app's storage and returns the upload object with its guid (see app-files-storage). Security: Only declared fetch_domains are allowed. Private IPs blocked (SSRF protection). Response shape: res is WHATWG-style - res.ok (true only on 2xx), res.status, res.statusText, res.headers.get('content-type'), await res.text(), await res.json(). The usual guard works: if (!res.ok) throw new Error(\HTTP ${res.status}`)`.

fetch is for third-party APIs only. Don't fetch the platform's own services at a.gipity.ai (llm/image/tts/location/...) - the sandbox blocks every *.gipity.ai fetch, and fetch_domains: ['a.gipity.ai'] does not unblock it. Call those through the injected app-service capabilities below instead.

Declare every host the call touches. fetch_domains is per function in gipity.yaml; a redirect to another host is re-checked, so list that host too:

- name: get-weather
  auth: public
  fetch_domains: [api.example.com]

Verifying webhook signatures (via crypto)

A provider webhook (Stripe, GitHub, Shopify, Slack, Svix/Clerk) signs the raw request body. Recompute the signature with crypto.hmac(...) over ctx.rawBody (never a re-serialized ctx.body), read the provider header from ctx.headers, and compare with crypto.timingSafeEqual. Full recipe, per-provider header shapes and the crypto API: app-development-reference.

App services from a function (via llm, image, tts, ...)

The platform's own services are injected as capabilities on the second argument - peers of db and fetch - so a function calls them server-to-server with no token and no fetch:

export default async function summarize(ctx, { db, llm }) {
  const { rows } = await db.query('SELECT body FROM notes WHERE user_guid = $1', [ctx.auth.userGuid]);
  const { text } = await llm({ model: 'fast', messages: [
    { role: 'user', content: 'Summarize:\n' + rows.map(r => r.body).join('\n') },
  ]});
  await db.query('INSERT INTO summaries (user_guid, summary) VALUES ($1, $2)', [ctx.auth.userGuid, text]);
  return { summary: text };
}
- name: summarize
  auth: user
  tables: ['notes', 'summaries']
  services: ['llm']      # required - declares which services this function may call

Secrets (via secrets)

Never hardcode an API key, password, or token in source. Store it as a secret and read it at runtime - encrypted at rest (AES-256-GCM), write-only (values never leave the server), and never shipped to the browser.

const apiKey = await secrets.get('OPENAI_API_KEY'); // null if unset

Set one from the CLI (UPPER_SNAKE_CASE names):

gipity secrets set OPENAI_API_KEY sk-...            # this project only
gipity secrets set OPENAI_API_KEY sk-... --account  # shared across ALL your projects
gipity secrets list          # names + masked previews (never values)

Scope: a secret is either project-scoped (this app) or account-scoped (shared across all your projects - set a key like OPENAI_API_KEY once and every app inherits it). secrets.get(name) resolves the project secret first, then falls back to the account secret of the same name, so a project can override the shared default.

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 (gipity fn logs <name>).

Related Skills