# 280 capability reference

Generated from the `@280/contracts` capability catalog (version 1.0.0). This is the authoritative list of what a 280 app may do; `setup.md` links here. If a required operation is not listed as supported, stop and report it rather than working around the network boundary.

## Supported stacks

Your app must listen on port 8080 (the platform sets PORT=8080). Next.js and static sites build automatically; any other stack ships a repo root Dockerfile that listens on that port.

| Stack | Notes |
| --- | --- |
| Static HTML | Any static site (HTML, CSS, JS, assets) — Served with SPA fallback to index.html |
| Next.js | Server rendering (SSR, React Server Components) |
| Next.js | API routes and route handlers |
| Next.js | Static pages (SSG) |
| Next.js | Incremental Static Regeneration (ISR) — On disk cache is per instance, not durable across restarts |
| Next.js | Server Actions |
| Next.js | Middleware |
| Next.js | Image optimization (next/image) |
| Next.js | Native and WebAssembly dependencies — Full container, compiled at build time |
| Other stacks | Any language or framework via a repo root Dockerfile — Used as is; the app must listen on port 8080 |

## SDK capabilities

Every external integration goes through `@two80/sdk`. The container reaches only the 280 API, which authorizes each call for the current app and user; the app holds no provider credentials.

| Capability | Slug | Operations |
| --- | --- | --- |
| Google Sheets | `google-sheets` | `read`, `append`, `update`, `deleteRows` |

Declare every integration the app uses in `280.json` as an alias mapped to its capability and the operations it calls, so push can gate the deploy until that alias is connected. The alias (`todos` below) is your app-chosen name; 280 binds it to a real resource at connect time.

    { "integrations": { "todos": { "capability": "google-sheets", "operations": ["read", "append", "update", "deleteRows"] } } }

### Request scoping

Every capability is a factory that takes the **incoming request** and returns a typed client scoped to the current caller. Nothing is global or cached across requests: the SDK reads the gateway-stamped identity header off the request you pass and forwards it, so the 280 API can authorize the call for this app and this user. Pass whatever exposes the request headers where you handle the request:

- a Fetch `Request` (its `.headers` are read for you): `googleSheets(request)`
- Next's `headers()` result: `googleSheets(await headers())`

Read identity from the same request the same way, via `identity()`:

    import { identity } from "@two80/sdk";

    export async function GET(request: Request) {
      const { user, can, anonymous } = await identity(request);
      user.email;             // resolved by the gateway, never by app code
      can("approvals.edit");  // true when the viewer holds that feature role
      anonymous;              // true for a public app's no-session visitor
    }

### Framework example: Google Sheets

    import { googleSheets } from "@two80/sdk";

    // In a Next.js route handler or Server Action, pass the incoming request.
    export async function POST(request: Request) {
      const sheets = googleSheets(request);
      // "todos" is the alias from 280.json, not a spreadsheet id.
      await sheets.read({ resource: "todos", range });            // -> { range, majorDimension, values }
      await sheets.append({ resource: "todos", range, values });  // -> { updatedRange, updatedRows, updatedCells }
      await sheets.update({ resource: "todos", range, values });  // -> { updatedRange, updatedRows, updatedCells }
    }

`resource` is the alias you declared in `280.json` (e.g. `"todos"`), not a spreadsheet id: 280 binds that alias to a real sheet at connect time, so the app never carries a raw sheet id. `range` is A1 notation (e.g. `Sheet1!A1:C10`), and `values` is a 2D array. A failed call throws `IntegrationRequestError` with `{ code, message, status, retryable }`. Full package docs: <https://www.npmjs.com/package/@two80/sdk>.

## Explicitly unsupported

The container runs full Node 20, so native modules, child processes, and local disk writes all work. What the boundary forbids:

| Not supported | Do this instead |
| --- | --- |
| Unrestricted outbound network | Default deny; containers can reach only the 280 SDK API host (others get HTTP 520) |
| Raw TCP outbound (e.g. Postgres on :5432) | Use an available @two80/sdk capability instead |
| Background work while idle (setInterval, polling loops) | A single instance sleeps after about 2 minutes idle; use request handlers |
| Websockets | Edge proxying of upgrades is unverified; poll instead |

Provider SDKs, raw API calls, connection strings, and any app-managed credential are unsupported by design: route the need through an `@two80/sdk` capability, or report it as missing.
