# Connect real data (resources)

Volly apps get a lot more useful once they can show real company data: a
dashboard over your production database, a report built on PostHog
analytics, a tool that reads an Airtable base. **Resources** are how you
connect that data. An admin sets one up once, then decides which apps are
allowed to use it, and app code never sees a password or API key.

## Connecting a resource (admins)

Head to **Settings → Resources** and add one. What you need depends on the
type:

- **Postgres.** Volly never asks for your admin database credentials.
  Setup generates a SQL script for you to run yourself, in Supabase's SQL
  Editor, `psql`, wherever you manage the database. That script creates a
  sandboxed, read-only role scoped to whichever schemas you pick, and Volly
  only ever stores that role's connection string. Your database needs to be
  reachable from the public internet for any of this to work.
- **PostHog.** Paste a personal API key with read-only scopes, or click
  **Connect with PostHog** and approve the requested scopes. No key to
  create or rotate that way.
- **Airtable and other REST APIs.** Paste an API key (a personal access
  token for Airtable, or whatever the service calls it) plus the base URL
  apps should be confined to.
- **Google Sheets.** Connects through Google OAuth, no key at all. By
  default each teammate links their own Google account the first time an
  app touches the resource, so every read or write is attributed to them and
  limited to sheets they can already open. Flip **Share credentials between
  users** if one connected account should serve everyone instead.

Once it's connected, open the resource and choose which apps can use it, and
whether they get read-only or read-and-write access.

## Requesting access (everyone else)

Building an app that needs one of these? Open the project page, pick a
resource from the org's list, add a short note on why, and submit. An admin
reviews it in Settings → Resources: approve grants the access, deny sends a
reason back to your project page. That page always shows what's granted,
pending, and denied for your app.

An app that hasn't been granted a resource simply can't see it. There's
nothing to guess at.

## Calling a resource from your app

Once access is granted, calling a resource is just `fetch`, no keys to
manage. Every call needs the app running in a signed-in session and the
`X-Volly-Resource: 1` header attached (that's a CSRF guard, not a secret;
just include it).

### Postgres

```js
const res = await fetch('/__volly/resources/crm-data/query', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'X-Volly-Resource': '1' },
  body: JSON.stringify({
    sql: 'select name, plan from customers where plan = $1 limit 50',
    params: ['enterprise'],
  }),
})
const { rows, rowCount } = await res.json()
```

Errors come back as JSON with the real database message, so an agent
writing the query can debug it directly: `{ "error": "column \"nope\" does
not exist (42703)" }`, for instance. Results cap at 2&nbsp;MB, so add a
`LIMIT` if you hit that.

### REST APIs (PostHog, Airtable, custom)

Take any `GET` endpoint from the vendor's own docs, drop their base URL, and
prepend `/__volly/resources/<alias>/proxy`:

```js
// PostHog docs: GET /api/projects/<id>/insights
const insights = await fetch('/__volly/resources/posthog/proxy/insights?limit=10', {
  headers: { 'X-Volly-Resource': '1' },
}).then((r) => r.json())

// Airtable
const tasks = await fetch('/__volly/resources/airtable/proxy/Tasks?maxRecords=50', {
  headers: { 'X-Volly-Resource': '1' },
}).then((r) => r.json())
```

`GET` and `HEAD` always work; other methods return `405` unless the app was
granted read-and-write. Most vendor SDKs won't work here since they tend to
hardcode their own host or attach their own key, so plain `fetch` is the
supported path.

### Google Sheets and other per-viewer resources

If a teammate hasn't linked their Google account yet, a call comes back
`403` with a link to send them to:

```json
{
  "error": "connect your google account to use this resource",
  "code": "oauth_not_connected",
  "connectUrl": "/__volly/resources/team-sheet/connect"
}
```

Render `connectUrl` as a real link the viewer clicks, not a fetch or an
iframe (Google won't run consent inside one). Add `?return_to=<path>` so
they land back where they were, and once connected, the same call just
works.

### Common errors

| Status    | Meaning                                                                       |
| --------- | ----------------------------------------------------------------------------- |
| 401       | Session expired, reload to sign in again                                      |
| 403       | Missing the `X-Volly-Resource` header, or an OAuth connect prompt (see above) |
| 404       | Unknown resource, or this app hasn't been granted it                          |
| 405       | Tried to write without a read-and-write grant                                 |
| 429       | Too many requests, retry shortly                                              |
| 502 / 504 | The upstream database or API failed or timed out                              |

## Building with AI agents

Agents connected through [Volly's MCP server](https://volly.so/docs/mcp) can call
`list_resources` to see what's available, what a given app can already
reach, and copy-ready code for using it. Ask an agent to "build a dashboard
on our CRM data" and it knows exactly what to call; granting access itself
stays a human, admin action in the dashboard.

Curious how this stays safe with credentials an app never sees? See
[how resources stay secure](https://volly.so/docs/external-resources-security).