# Supabase astrology API, Edge Functions and Postgres

> Ship horoscopes, natal charts or tarot inside a [Supabase](https://supabase.com) project in about twenty minutes, with the key in project secrets and never in the browser.

There are three ways in and they compose. An Edge Function calls RoxyAPI and hands JSON back to your app. A foreign data wrapper turns endpoints into tables you read with plain SQL. Your own tables keep whatever you want to keep, under row level security. This page covers all three. Which endpoint to call for a given reading lives in the domain guides at the bottom.

## The call that proves it

Run this before you wire anything, so a failure later is never ambiguous:

```bash
curl "https://roxyapi.com/api/v2/astrology/horoscope/aries/daily" \
  -H "X-API-Key: $ROXY_API_KEY"
```

No key yet? One key covers every domain on any paid plan, see [pricing](/pricing).

## Put the key in project secrets

```bash
supabase secrets set ROXY_API_KEY=sk_your_key_here
```

An Edge Function reads it with `Deno.env.get('ROXY_API_KEY')`. Nothing else in your project needs it.

**Warning: **Never a client-visible variable.** Anything your browser bundle can read is public, so a secret key does not belong in a `NEXT_PUBLIC_`, `VITE_` or `EXPO_PUBLIC_` variable, and it does not belong in a table your client can select from. Browser code that has to call us directly uses a publishable `pk_` key locked to your origin, or a ready-made embed, see [widgets](/docs/widgets).**

## Path 1, call RoxyAPI from an Edge Function

Edge Functions run Deno, which imports npm packages directly, so the typed SDK works with no build step and no bundler. Give the function its own `deno.json`:

```json
{
  "imports": {
    "@roxyapi/sdk": "npm:@roxyapi/sdk@1"
  }
}
```

Then the function itself:

```ts
import { createRoxy } from '@roxyapi/sdk';

const roxy = createRoxy(Deno.env.get('ROXY_API_KEY')!);

const SIGNS = [
  'aries', 'taurus', 'gemini', 'cancer', 'leo', 'virgo',
  'libra', 'scorpio', 'sagittarius', 'capricorn', 'aquarius', 'pisces',
] as const;

Deno.serve(async (req) => {
  const { sign } = await req.json();
  if (!SIGNS.includes(sign)) {
    return Response.json({ error: 'Send a zodiac sign', code: 'bad_request' }, { status: 400 });
  }

  const { data, error } = await roxy.astrology.getDailyHoroscope({ path: { sign } });
  if (error) return Response.json(error, { status: 502 });

  return Response.json(data);
});
```

A request body is untyped, so check the sign rather than forwarding it. Anything else is a round trip that can only fail.

Deploy it with `supabase functions deploy daily-horoscope`. The reading comes back with `overview`, `love`, `career`, `health`, `finance`, `advice`, `luckyNumber`, `luckyColor`, `compatibleSigns`, `moonSign`, `moonPhase` and `energyRating`, so one call fills a whole card.

### Anything with a birth chart needs a place first

Charts need latitude, longitude and a timezone. Never ask a user for coordinates, resolve the city instead:

```ts
const { data: found, error: cityError } = await roxy.location.searchCities({
  query: { q: 'New York', limit: 1 },
});
if (cityError) throw cityError;

const city = found.cities[0];

const { data: chart } = await roxy.astrology.generateNatalChart({
  body: {
    date: '1990-01-15',
    time: '14:30:00',
    latitude: city.latitude,
    longitude: city.longitude,
    timezone: city.timezone,
  },
});
```

The city record carries an IANA `timezone` such as `America/New_York`, which resolves to the correct offset for the birth date. A decimal offset knows nothing about daylight saving, so a January birth and a July birth would share one wrong answer.

## Path 2, query RoxyAPI from Postgres with SQL

Supabase ships a generic OpenAPI foreign data wrapper, so RoxyAPI endpoints can be read as tables. No client, no function, no deploy. This suits catalogue and daily content best: decks, hexagrams, dream symbols, crystals, sign reference data and a daily reading per sign.

Enable the extension and the wrapper once:

```sql
create extension if not exists wrappers with schema extensions;

create foreign data wrapper wasm_wrapper
  handler wasm_fdw_handler
  validator wasm_fdw_validator;
```

Then point a server at the spec. Take the current version and checksum from the [wrapper catalog](https://fdw.dev/catalog/openapi/), and store the key in [Vault](https://supabase.com/docs/guides/database/vault) rather than inline for anything but a scratch project:

```sql
create server roxy_server
  foreign data wrapper wasm_wrapper
  options (
    fdw_package_url 'https://github.com/supabase/wrappers/releases/download/wasm_openapi_fdw_v0.2.1/openapi_fdw.wasm',
    fdw_package_name 'supabase:openapi-fdw',
    fdw_package_version '0.2.1',
    fdw_package_checksum '12c902f3089e18142a1d8d35c66b9ceb85c193224229687bd929aff6b44cddde',
    base_url 'https://roxyapi.com/api/v2',
    spec_url 'https://roxyapi.com/api/v2/openapi.json',
    api_key 'sk_your_key_here',
    api_key_header 'X-API-Key',
    api_key_prefix ''
  );

create schema if not exists roxy;
```

`import foreign schema` reads the spec and writes the tables for you:

```sql
import foreign schema openapi from server roxy_server into roxy;

select id, name, element from roxy.astrology_signs limit 3;
```

Columns come typed from the published schema, and every table also carries an `attrs jsonb` column holding the whole response row, so nothing is lost when a field has no column.

### Readings that take an input

A value in a path is filled from the `where` clause:

```sql
create foreign table roxy.daily_horoscope (
  sign text,
  overview text,
  love text,
  career text,
  lucky_number integer,
  lucky_color text,
  attrs jsonb
)
server roxy_server
options (endpoint '/astrology/horoscope/{sign}/daily');

select sign, lucky_number, lucky_color, overview
from roxy.daily_horoscope
where sign = 'aries';
```

Readings that take a body are a table with the body written into it:

```sql
create foreign table roxy.life_path (
  number integer,
  type text,
  calculation text,
  meaning jsonb,
  has_karmic_debt boolean,
  attrs jsonb
)
server roxy_server
options (
  endpoint '/numerology/life-path',
  method 'POST',
  request_body '{"year":1990,"month":7,"day":15}'
);

select number, type, calculation from roxy.life_path;
```

Field names translate across the two conventions, so `luckyNumber` reads into `lucky_number` and `hasKarmicDebt` into `has_karmic_debt`.

### What this path cannot do

Worth knowing before you build on it, because the first two fail quietly rather than loudly.

- **The body is fixed per table.** One table is one request, so this fits a deck or a daily reading, not a different birth chart per visitor. Charts that vary per user belong in Path 1.
- **`import foreign schema` also writes a table per body-taking endpoint**, and those carry no body, so they return an error until you add one or drop them. Keep the tables you actually use.
- **Reads only.** Foreign tables here are for `select`.
- **The server knows one base.** A domain spec such as `/api/v2/tarot/openapi.json` declares its own base, so set `base_url` to match it. Pointing a domain spec at the bare `/api/v2` base returns zero rows instead of an error.

## Path 3, keep the readings in your own tables

Content that is the same for everyone is worth storing once a day rather than fetching per visitor:

```sql
create table daily_reading (
  sign text not null,
  reading_date date not null default current_date,
  payload jsonb not null,
  primary key (sign, reading_date)
);

alter table daily_reading enable row level security;

create policy "daily readings are public"
  on daily_reading for select
  using (true);
```

Fill it from a scheduled function, or straight from a foreign table if you took Path 2:

```sql
insert into daily_reading (sign, payload)
select sign, attrs from roxy.daily_horoscope where sign = 'aries'
on conflict (sign, reading_date) do nothing;
```

Anything personal is the opposite case. Keep a chart keyed to the person who owns it:

```sql
create table saved_chart (
  id uuid primary key default gen_random_uuid(),
  user_id uuid not null references auth.users on delete cascade,
  chart jsonb not null,
  created_at timestamptz not null default now()
);

alter table saved_chart enable row level security;

create policy "people read their own charts"
  on saved_chart for select
  using (auth.uid() = user_id);
```

Birth data is personal data, so store the least you need and delete it when the account goes. What we do with a request on our side is on [data protection](/docs/data-protection). Which readings are safe to cache and for how long is in [caching and cost](/docs/guides/caching).

## Gotchas

- **Reserved secret names.** Names beginning `SUPABASE_` are managed by the platform and cannot be set, so call the key `ROXY_API_KEY`.
- **Edge Functions need the header, not a cookie.** Auth for RoxyAPI is the `X-API-Key` header on every request, which the SDK sets for you.
- **Prefer IANA timezones.** `America/New_York` is correct for both a January and a July birth. A decimal offset is correct for neither.
- **Errors come back as `{ error, code, doc_url }`.** There is no success wrapper to unpack, and a 400 lists every bad field at once rather than the first.
- **Retry 429 and 5xx only.** A 400 will fail the same way every time.
- **A paused project stops its Edge Functions**, and every reading that depends on them stops with them.

## What to build next

- **Domain guides**, for which endpoints to call in what order:
  - [Western Astrology](/docs/guides/astrology), [Vedic Astrology](/docs/guides/vedic-astrology), [KP Astrology](/docs/guides/kp), [Forecast](/docs/guides/forecast), [Human Design](/docs/guides/human-design), [Chinese Astrology](/docs/guides/chinese-astrology), [Feng Shui](/docs/guides/feng-shui), [Mesoamerican Astrology](/docs/guides/mesoamerican-astrology), [Vastu](/docs/guides/vastu), [Numerology](/docs/guides/numerology), [Kabbalah](/docs/guides/kabbalah), [Tarot](/docs/guides/tarot), [Biorhythm](/docs/guides/biorhythm), [Ayurveda](/docs/guides/ayurveda), [I Ching](/docs/guides/iching), [Crystals](/docs/guides/crystals), [Dreams](/docs/guides/dreams), [Angel Numbers](/docs/guides/angel-numbers)
- [SDK](/docs/sdk): the same typed client in TypeScript, Python, PHP, C# and Go.
- [Remote MCP](/docs/mcp): connect an agent to live calculations instead of wiring endpoints one at a time.
- [Astrology on Supabase](https://github.com/RoxyAPI/astrology-supabase-starter): this page as one deployable repo. Both paths above wired together, the schema, the policies and the demo, MIT. Start here if you would rather fork than assemble.
- [Templates](/docs/templates): every other MIT app, including a companion built on Supabase auth, Postgres and pgvector.
- [API reference](/api-reference): try any endpoint in the browser.

## FAQ

**Does RoxyAPI work with Supabase Edge Functions?**

Yes. Edge Functions run Deno, which imports npm packages directly, so the typed SDK is one line in `deno.json` and one import in the function. Set the key with `supabase secrets set` and read it with `Deno.env.get`. No build step, no bundler and no extra dependency.

**Can I query an astrology API from Postgres with SQL?**

Yes. Supabase ships a generic OpenAPI foreign data wrapper, and RoxyAPI publishes a spec it can read, so `import foreign schema` writes the tables and you query them like any other table. It suits catalogue and daily content. Readings that change per visitor are better from an Edge Function, because a foreign table sends one fixed request body.

**Where should the API key live in a Supabase project?**

In project secrets, read inside an Edge Function with `Deno.env.get`, or in Vault when a foreign data wrapper needs it. Never in a client-visible variable and never in a table your browser code can select from. Browser code that must call us directly uses a publishable key locked to your origin.

**Do I need a separate SDK for Supabase?**

No. The TypeScript SDK on npm is the one you use, because Edge Functions resolve npm packages natively. There is nothing Supabase-specific to install.

**Can I cache horoscopes in my Supabase database?**

Yes, and for daily content you should. A daily reading is the same for everyone with that sign, so write it once a day into your own table and serve every visitor from Postgres. Keep anything personal, such as a saved birth chart, in a separate table under row level security keyed to the signed-in user.
