client agent
Supabase
The only agent that knows what the endpoint *is* rather than what it cost — so it can tell a slow query from a policy that refused one.
- package
@sixty-sh/supabaseon npm- runs on
- Alongside the browser agent, in a project whose backend is Supabase.
- source
- packages/supabase
Installing it
For a frontend with no server of your own to deploy: runs entirely in the browser with a public, origin-pinned key. If the project has any server, use browser or node.
The install is written as a prompt for the coding agent you already have open, not as a checklist for you. That is deliberate: it names what must be true when the install is finished rather than which files to edit, because where the code goes depends on the framework and putting it in the wrong place fails silently. An agent can read your repository and work that out; a paragraph on a documentation page cannot.
The same text is what install_sixty returns through the MCP server and what the collector serves at /v1/setup?kind=lovable. There is one copy of it.
the Supabase install, in full
Add sixty.sh performance monitoring to this app. It measures Supabase query
shape, page speed, stuck loading states and browser errors, and reports
anonymously.
Before anything else: does this project run a server of its own? A Lovable
app often grows one — an API route, a serverless function, an Express or
FastAPI backend added later, a Supabase edge function you deploy. If it has
one, STOP and ask for the "browser" install instead, or the "node" or
"python" one if the server holds logic worth measuring. Those keep the key
server-side behind a proxy and can measure the server as well as the page.
This install is for the case where the frontend is genuinely the only code
you deploy, and it trades a world-readable key for that.
If that is this project, work out which shape it is, because it decides where
the code goes and there is no error if you get it wrong — it just silently
never runs:
A. Client-only Vite SPA. There is a createRoot(...) call in a file like
src/main.tsx and no server entry.
B. Server-rendered framework app — React Router v7 framework mode, Remix,
or similar. Signs: an entry.client.tsx / entry.server.tsx pair, a
root.tsx, a hydrateRoot(...) call, or a routes.ts.
Then:
1. Add two dependencies: @sixty-sh/supabase and @sixty-sh/browser.
2. Call init() from "@sixty-sh/browser" exactly once, in the browser.
init({ key: "sixty_pk_YOUR_KEY" })
In shape A, module scope in the app entry is fine.
In shape B it MUST go in the client entry (entry.client.tsx), after
hydration. Do not put it in a shared module that the server also
evaluates: there it runs during server rendering, finds no window,
returns null, and reports nothing at all — with no error to tell you.
3. Find where the Supabase client is created. Search the repository for
"createClient(" rather than guessing a path — it is often in
src/integrations/supabase/client.ts but in this kind of project it may be
anywhere. Then, at module scope in that same file:
import { withSixty } from "@sixty-sh/supabase"
withSixty(supabase, { key: "sixty_pk_YOUR_KEY", service: "my-app" })
Use the client that already exists. withSixty patches the PostgREST
prototype and returns the same object it was given, so it instruments every
query in the app without replacing the export.
Constraints — correctness requirements, not style preferences:
- Do NOT create a second Supabase client. There must still be exactly one.
- Do NOT edit vite.config.ts and do NOT add a Vite plugin.
- Do NOT change any existing query, component, or hook. This is
instrumentation only; no behaviour should change.
- The key above is PUBLIC by design: write-only, pinned to one service, and
refused from any origin it does not list. Paste it literally. Do not move it
into an environment variable or a secret.
- Do NOT add any analytics library, user id, session id, or cookie. This agent
is deliberately anonymous and must stay that way.
When you are done, tell me which shape you found and which files you changed,
then publish the app. Findings need two published releases with traffic under
each before anything can be compared.It needs a public key — pinned to your origins, because an app with no server of its own has nowhere to keep a secret.
What it measures
| signal | unit | what it means |
|---|---|---|
errors | error rate | a larger fraction of calls are throwing |
new_error | occurrences | an error that did not occur in the previous release |
latency | ms per call | this operation takes longer end to end than it used to |
silent_empty | — | The query runs and succeeds, and returns no rows where it used to return plenty. Nothing reports an error, so the page just renders blank — this is what a broken permission rule looks like from the outside. |
auth_failures | — | The server is turning these away on permission grounds rather than failing. People see an empty page or a save that quietly does nothing. |
Failures it can name
Not signals. These are the classes it puts on an error, which is what makes the difference between “a call failed” and “a policy refused it”.
rls_denied | A row-level security policy refused the statement (Postgres 42501). It reaches the browser as an empty list and your logs as nothing at all. |
|---|---|
schema_missing | A column, table, relationship or function the code expects is not in the database. |
constraint_violated | A write was rejected by a database constraint. |
jwt_expired | A session token was expired or invalid where one was required. |
realtime_duplicate_subscription | One topic subscribed concurrently three times or more — an effect with no teardown, seen from the wire. |
realtime_channel_error | A channel reported CHANNEL_ERROR or TIMED_OUT instead of subscribing. |
Where it hooks in
- supabase-js — PostgREST calls, realtime channels and auth, instrumented where they are made.
- Vite — A plugin, for projects built with one.
Databases
- PostgREST — The request describes the query, so the table, the filters and the failure code are all legible without a statement.
What only this one does
- A name for the failure — Every other agent can say a call failed. This one can say a policy refused it, a column is missing, or the token had expired — because a PostgREST request describes the query rather than merely costing something.
- Realtime as well as reads — Channels that error, time out, or get subscribed three times over because an effect has no teardown.
What it cannot do
- This is an addition to the browser or Lovable agent, not a replacement for one. It explains Supabase failures; it does not measure the page.
- A project with no server of its own uses a public, origin-pinned key. If it has grown an API route or an edge function, take the browser or Node tier instead — the key stays server-side and the server gets measured too.
Configuration
Every agent reads the same four variables, and DRIFT_* still answers everywhere SIXTY_* does — the product was renamed and that name is not ours to retire from other people’s deployments.
SIXTY_API_KEY | Without it the agent stays inert and says so. It never guesses, never retries against an unknown endpoint, and never throws. |
|---|---|
SIXTY_SERVICE | What to call this service. Defaults to the project name where one is legible. |
SIXTY_RELEASE | The one that matters most. Picked up automatically on Vercel, Render, Railway, Fly, Heroku and GitHub Actions; set it to the commit SHA anywhere else. Without it every measurement lands in a single nameless bucket and no comparison can ever be made. |
SIXTY_ENDPOINT | Where to report. Defaults to http://localhost:4319, which is right on a laptop and wrong the moment the app is served to anyone else. |
The rest — flush interval, sample rate, what to instrument — is in the package’s own README, which is where it can stay true as the agent changes.