Theme
Setting up Supabase
Jua runs on Supabase. It is always the database, and with its keys set it also signs customers and staff in (Auth, with authenticator-app codes for staff), carries live prices and fills to every open page (Realtime), and keeps ID photos (Storage). Real money runs with all of it switched on: without Supabase Auth, staff move money with a password alone.
Do these steps once per environment, in this order. Production gets a project of its own; never share one with a test or demo deployment. Every setting below is made in the Supabase dashboard. infra/supabase/config.toml is for the local Supabase CLI stack only: never supabase config push it to a hosted project.
1. Create the project
- At supabase.com, create a project in the region nearest the server that runs Jua: every request the API answers reads the database, so that distance matters most.
- Choose a paid plan for real money: it brings daily backups, and point-in-time recovery (step 9) is offered only there.
- Generate a strong database password and keep it in the team's password manager. It is part of both connection strings.
2. Connect the database
In the dashboard's Connect panel, copy two strings. Both use the pooler host aws-0-<region>.pooler.supabase.com, whose username is postgres.<project-ref>:
| GitHub Actions secret | Connection | Port | Used by |
|---|---|---|---|
DATABASE_URL | Transaction pooler | 6543 | The apps |
DIRECT_URL | Session pooler | 5432 | db:migrate in the deploy, and the db:* scripts |
Not the db.<project-ref>.supabase.co host: it is IPv6-only, and most servers and home networks cannot reach it.
The deploy runs bun run db:migrate, and the migrations create everything Supabase needs: the profiles row for each new auth.users user, row-level security on every table, the Realtime publication and its channel policies, and the private kyc-documents storage bucket. Never run drizzle-kit push or drizzle-kit migrate against Supabase (docs/database.md in the repository).
To check from a checkout on your own machine, with the same DIRECT_URL in packages/db/.env.local:
bash
bun run db:status # connects, and lists pending migrations: none after a deploy3. Sign-in methods
Authentication → Sign In / Providers:
| Provider | Setting | Why |
|---|---|---|
| On, with Confirm email on | Only a confirmed address becomes an account's contact | |
| Web3 Wallet → Ethereum | On | Continue with wallet on the website |
| Web3 Wallet → Solana | On | Phantom, Solflare and Trust's Solana wallets. With Ethereum alone, a Solana wallet is refused after it has signed |
| Phone | Only if customers sign in by phone (the mobile app does) | Needs an SMS provider: Twilio, MessageBird, Vonage and Textlocal reach African numbers. Keep Confirm phone on |
To see which wallet providers are on without the dashboard, ask the Auth server to sign nothing in:
bash
curl -s -X POST "https://<project-ref>.supabase.co/auth/v1/token?grant_type=web3" \
-H "apikey: <anon key>" -H "content-type: application/json" -d '{"chain":"solana"}'It answers web3_provider_disabled while Solana is off, and "message is too short" once it is on. Try "ethereum" the same way.
The Telegram Mini App does not use Supabase Auth: the API checks Telegram's own launch data.
4. Site addresses
Authentication → URL Configuration:
- Site URL:
https://juamarkets.com - Redirect URLs:
https://juamarkets.com/**
A password reset lands on /reset-password, and wallet sign-in signs its message for /login. Supabase refuses a wallet message for a page that is not listed or not https, so wallet sign-in cannot work while the site is reached as http://<ip>:<port>.
5. Email delivery
Supabase's built-in email sender delivers only to the project's own team members. Customers' confirmation and password-reset emails need Jua's own SMTP server: set it in Auth's SMTP settings (the sender address, host, port, username and password from your email provider). Then sign up with an address outside the team and check that the confirmation email arrives.
6. Authenticator codes
Authentication → Multi-Factor: turn TOTP (authenticator app) on, for enrolment and for verification.
Every staff sign-in to the portal then asks for a code, and every staff action that moves money or changes an account needs a session confirmed with one. Customers are offered an authenticator app after sign-up and in Settings → Security.
7. Keys
From Project Settings → API, add to the repository's Actions settings (Settings → Secrets and variables → Actions):
| Kind | Name | Value |
|---|---|---|
| variable | NEXT_PUBLIC_SUPABASE_URL | https://<project-ref>.supabase.co |
| variable | NEXT_PUBLIC_SUPABASE_ANON_KEY | The anon (publishable) key: public by design |
| secret | SUPABASE_SERVICE_ROLE_KEY | The service-role (secret) key. Server only: never in a variable, a browser or a chat |
| secret | SUPABASE_JWT_SECRET | Only if the project still signs tokens with the legacy HS256 secret |
Then run a full deploy: the two public values are built into the images. The service-role key lets the API confirm a customer's email or phone, send live updates to every page, and delete an account's sign-in and ID photos when it is closed. Without it the API copies no contact into a profile.
Once Supabase Auth is on, the demo login is no longer offered. Rotating any of these keys: Keys and roles.
8. Staff accounts
One account per person, never a shared one. From a checkout on your own machine, with production's DIRECT_URL in packages/db/.env.local:
bash
bun run db:staff alice@example.com admin # admin or resolver; prints the password once
bun run db:staff alice@example.com admin --reset # a new password and authenticator; ends their sessions
bun run db:staff alice@example.com user # takes the staff role away; ends their sessionsHand each password over privately. At the first sign-in at the staff portal, the page shows a QR code: the staff member scans it with an authenticator app (Google Authenticator, 1Password, Authy) and enters the code. Every later sign-in asks for the current code.
Then, on the portal's Settings page, check that two-step verification (crypto.requireMfa) is on. It is on by default, and the page warns when it is not in force.
9. Backups
Turn on point-in-time recovery (a paid add-on in the project's settings) before real money, then rehearse a restore into a scratch project and run bun run db:status against it. Repeat the rehearsal every quarter (Recurring tasks).
10. Check it works
- [ ]
bun run db:statuslists no pending migrations. - [ ] The
curlin step 3 answers "message is too short" for both"ethereum"and"solana". - [ ] On the website, Continue with wallet signs in with MetaMask and with a Solana wallet.
- [ ] An email sign-up from an outside address receives its confirmation email, and "Forgot password?" sends a link.
- [ ] A staff member signs in to the portal with password and code, and the Settings page shows no two-step warning.
- [ ] A market open in two browsers: a trade in one moves the price in the other without a reload.
Housekeeping can also be scheduled from the database with Supabase's pg_cron and pg_net: Housekeeping.