> ## Documentation Index
> Fetch the complete documentation index at: https://docs.xquik.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Twitter API in Supabase with SQL, pg_cron & webhooks

> Call the Twitter API from Supabase Edge Functions, SQL, and pg_cron. Receive signed X webhook events in Postgres, stored once and verified. See examples.

> For the complete documentation index, see [llms.txt](https://docs.xquik.com/llms.txt).

Supabase is a Postgres platform with Edge Functions, Vault, and pg\_cron. This
guide brings the Twitter API into a Supabase project through Xquik. You read
tweets and profiles from SQL, schedule reads with pg\_cron, and run actions from
row inserts. Signed X webhook events land in a Postgres table, each one stored
once.

[`supabase-xquik`](https://github.com/Xquik-dev/supabase-xquik) is a ready
Supabase project. Clone it and start it with 1 command. Xquik does not require
an X Developer app.

> **Typed calls in Edge Functions**
>
> The `x-twitter-scraper` SDK gives typed calls to every public operation.

> **SQL calls**
>
> `xquik.call` and `xquik.rows` reach every public operation from SQL.

> **Schedules**
>
> `xquik.schedule` runs any operation on a pg\_cron schedule.

> **Actions from rows**
>
> Insert a row into `xquik_actions` and the action runs.

> **Signed events**
>
> The `xquik-webhook` Edge Function verifies each event and stores it once.

> **Key in Vault**
>
> SQL reads the API key from Vault. It never sits in a table.

## Start the project locally

Create an [Xquik API key](https://docs.xquik.com/x-api-quickstart.md). Install Docker, then clone the
project.

```bash theme={null}
git clone https://github.com/Xquik-dev/supabase-xquik.git
cd supabase-xquik
XQUIK_API_KEY=xq_YOUR_KEY_HERE XQUIK_WEBHOOK_SECRET=YOUR_WEBHOOK_SECRET npx supabase start
```

`supabase start` applies the migrations and serves the Edge Functions. The key
goes to Vault for SQL and to the Edge Function secrets. The webhook secret goes
to the receiver. The CLI also reads both values from a `.env` file in the
project root.

## Call the Twitter API with the TypeScript SDK

`createXquik` in `_shared/xquik.ts` creates the typed SDK client from
`XQUIK_API_KEY`. Import it from any function in `supabase/functions`.

```typescript theme={null}
import { createXquik } from "../_shared/xquik.ts";

const xquik = createXquik();

Deno.serve(async () => {
  const profile = await xquik.x.users.retrieve("NASA");
  const page = await xquik.x.tweets.search({ q: "from:NASA", limit: 20 });
  return Response.json({ profile, tweets: page.tweets });
});
```

Each SDK method maps to 1 [API reference](https://docs.xquik.com/api-reference) operation. Your
editor completes parameters and response fields.

## Read Twitter data with SQL

`xquik.call` sends 1 operation and returns its JSON response. Pass the
operation ID from the API reference and a JSON object of parameters.

```sql theme={null}
select xquik.call('getUser', '{"id": "NASA"}');
```

`xquik.rows` returns the response list as 1 row per item. Use it for search,
timelines, and other list operations.

```sql theme={null}
select tweet->>'id' as id, tweet->>'text' as text
from xquik.rows('searchTweets', '{"q": "from:NASA", "limit": 20}') as tweet;
```

Path parameters such as `{id}` fill from the JSON object. The rest go in the
query string for `GET` and `DELETE`, or in the JSON body otherwise. A failed
call raises an error with Xquik's `error` and `message` fields.

The `xquik.operations` table lists every operation these functions accept.

```sql theme={null}
select operation_id, method, path from xquik.operations order by path;
```

Only the `service_role` and the database owner can run `xquik` functions.
Browser clients with the anon key cannot reach them.

## Schedule Twitter API reads with pg\_cron

`xquik.schedule` creates a pg\_cron job that sends 1 operation on a cron
schedule. Run it again with the same job name to change the job.

```sql theme={null}
select xquik.schedule(
  'nasa-timeline',
  '*/15 * * * *',
  'getUserTweets',
  '{"id": "NASA"}'
);
```

Each run adds a row to `public.xquik_actions`. The `xquik-collect` job copies
each finished response into that row every 10 seconds.

```sql theme={null}
select id, status, http_status, response->'tweets'
from public.xquik_actions
where operation_id = 'getUserTweets'
order by id desc
limit 5;
```

Remove a schedule with `cron.unschedule`.

```sql theme={null}
select cron.unschedule('nasa-timeline');
```

## Run actions from row inserts

Insert a row into `public.xquik_actions` to run an operation. A trigger sends
it as the row goes in, so the row lands as `sent`. Then `xquik-collect` marks
it `succeeded` or `failed`.

```sql theme={null}
insert into public.xquik_actions (operation_id, params)
values ('getUser', '{"id": "NASA"}');
```

`xquik.enqueue` does the same insert and returns the action ID.

```sql theme={null}
select xquik.enqueue('getTrends', '{"woeid": 1}');
```

Actions and schedules run `GET`, `POST`, and `DELETE` operations. Run `PUT`
and `PATCH` operations with `xquik.call`.

An app, a trigger, or a Supabase Database Webhook can add these rows. The table
has row level security with no policies. Insert from your server with the
service role key.

## Receive signed Twitter webhook events

The `xquik-webhook` Edge Function receives Xquik monitor events. It verifies
each signature and stores each event once in `public.xquik_events`.

Create a monitor for an account. Then point a webhook at the function URL. A
monitor bills while it is active, as [Billing](https://docs.xquik.com/guides/billing.md) explains.

```sql theme={null}
select xquik.call(
  'createMonitor',
  '{"username": "NASA", "eventTypes": ["tweet.new", "tweet.reply"]}'
);

select xquik.call(
  'createWebhook',
  '{"url": "https://YOUR_PROJECT_REF.supabase.co/functions/v1/xquik-webhook", "eventTypes": ["tweet.new", "tweet.reply"]}'
);
```

The webhook response returns `secret` only once. Store it as an Edge Function
secret, then send a test delivery.

```bash theme={null}
npx supabase secrets set XQUIK_WEBHOOK_SECRET=YOUR_WEBHOOK_SECRET
```

```sql theme={null}
select xquik.call('testWebhook', '{"id": "YOUR_WEBHOOK_ID"}');
```

Read stored events in the order they happened.

```sql theme={null}
select event_type, occurred_at, payload->'data'->>'text' as text
from public.xquik_events
order by occurred_at, stream_event_id;
```

The function answers each delivery with 1 status.

| Delivery | Status | Stored |
| - | - | - |
| Signed event | `200` | Once |
| Retry or duplicate of a stored event | `200` | No |
| `webhook.test` | `200` | No |
| Stale timestamp, bad signature, reused nonce | `401` | No |
| Any method other than `POST` | `405` | No |
| Signed body that is not an Xquik event | `200` | No |
| Database write failed | `503` | No |

A `503` makes Xquik retry the delivery later.

## Handle webhook edge cases

The function handles each case below. Keep the behavior when you edit it.

> **No Supabase JWT check**
>
> Xquik sends no Supabase JWT, so the default check answers `401`. The
> project sets `verify_jwt = false` for `xquik-webhook`. The HMAC signature
> authenticates each delivery instead.

> **Raw body**
>
> The function verifies the raw request bytes before it parses JSON.
> Re-serialized JSON can change bytes and fail the check.

> **Milliseconds**
>
> `X-Xquik-Timestamp` is Unix time in milliseconds. A value in seconds looks
> decades old and fails the 5-minute window.

> **Fast 2xx**
>
> Xquik waits 10 seconds for a `2xx`. The function stores the event and
> answers. Put slow work in `onEvent`, which runs after the response.

> **Signed retries**
>
> A retry keeps the body, `deliveryId`, and `streamEventId`. It gets a new
> timestamp, nonce, and signature, so it passes the nonce check.

> **Dedupe on streamEventId**
>
> `stream_event_id` is the primary key of `xquik_events`. A retry or a
> second webhook adds no second row.

> **Order by occurredAt**
>
> Deliveries can arrive in any order. Sort by `occurred_at`, then
> `stream_event_id`. An index covers that order.

> **Test alert**
>
> `webhook.test` has no `deliveryId` or `streamEventId`. The function
> answers `200` and stores nothing.

> **Several URLs**
>
> Each webhook has its own secret. Put every secret in
> `XQUIK_WEBHOOK_SECRET`, separated by commas.

> **Secret rotation**
>
> Create a new webhook for the same URL and add its secret. Delete the old
> webhook, then remove its secret.

> **No secrets in logs**
>
> The function never logs the secret, the signature, or the headers. Keep
> them out of any logging you add.

> **Nonce cleanup**
>
> The `xquik-nonce-cleanup` job deletes nonces older than 10 minutes. It
> runs every 5 minutes.

[Signature verification](https://docs.xquik.com/webhooks/verification.md) explains the signing string
and the replay window.

## Deploy to a hosted Supabase project

Link the clone to your project and push the migrations.

```bash theme={null}
npx supabase link --project-ref YOUR_PROJECT_REF
npx supabase db push
npx supabase functions deploy
npx supabase secrets set XQUIK_API_KEY=xq_YOUR_KEY_HERE XQUIK_WEBHOOK_SECRET=YOUR_WEBHOOK_SECRET
```

`functions deploy` reads `verify_jwt = false` for `xquik-webhook` from
`supabase/config.toml`. Then store the API key in Vault from the SQL editor.

```sql theme={null}
select vault.create_secret('xq_YOUR_KEY_HERE', 'xquik_api_key');
```

The SQL functions read the key from Vault. The Edge Functions read it from
their secrets.

## Common Supabase Twitter API questions

### How do I call the Twitter API from an Edge Function?

Call `createXquik()` from `_shared/xquik.ts`. Then call any SDK method, such as
`xquik.x.users.retrieve("NASA")`.

### Can I call the Twitter API from Postgres?

Yes. Run `xquik.call` for 1 JSON response or `xquik.rows` for 1 row per item.

### How do I schedule Twitter API reads in Supabase?

Run `xquik.schedule` with a job name, a cron schedule, and an operation ID.
Results land in `public.xquik_actions`.

### Why does my Supabase webhook function return 401?

The Supabase JWT check rejects the delivery before your code runs. Deploy
`xquik-webhook` with `verify_jwt = false`. A wrong secret also returns `401`.

### Where do I find the webhook secret?

The Create Webhook response returns it once. Store it with
`supabase secrets set XQUIK_WEBHOOK_SECRET`.

### Does a retried delivery create a duplicate row?

No. The function stores each `streamEventId` once and answers `200` for
repeats.

### Is this Supabase Twitter login?

No. This guide reads X data and receives X events. Use Supabase Auth for
sign-in with X.

## Source and next steps

* [supabase-xquik source](https://github.com/Xquik-dev/supabase-xquik)
* [TypeScript SDK](https://docs.xquik.com/sdks/typescript.md)
* [Webhooks overview](https://docs.xquik.com/webhooks/overview.md)
* [Signature verification](https://docs.xquik.com/webhooks/verification.md)
* [Tweet Search API](https://docs.xquik.com/api-reference/x/search-tweets.md)
* [Rate Limits](https://docs.xquik.com/guides/rate-limits.md)
* [Error Handling](https://docs.xquik.com/guides/error-handling.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.