Sync across devices
Off by default. See every machine's usage in one dashboard through a Supabase project you own.
Off by default. Garcon makes no network calls of its own until Enable Supabase sync is switched on in Settings; recording never depends on it and the local log stays each machine's source of truth. When it is on, every completion call is upserted into one table in a Supabase project you own, and the other machines' rows are pulled into a local cache, so each dashboard shows the union with a Device filter and sessions kept apart per machine.
Only you can read the table. It has row level security enabled with no policies, so the project's secret key (which bypasses RLS) is the only key that can touch it; the publishable key sees nothing. The secret key is stored owner-only on each machine and sent only to your project URL.
First machine
Start with garcon setup. For a browser-only setup, create or choose a project in Supabase, run the SQL under The table, then enter its URL and secret key in Settings → Sync. No Supabase CLI required.
The Supabase CLI does the whole setup, and the secret key goes straight from the CLI into the proxy without being printed. Log in once with supabase login, then, on the machine that runs garcon:
garcon connect-supabase --create-project --name "work laptop"- Checks that Garcon answers, the CLI supports
db query --project-ref, and the CLI is logged in (supabase orgs list). Capability checks happen before creating a project. If the CLI is too old, update it or use the browser setup above. - Creates a project named
garcon(check your organization’s plan and project limits) inus-east-1, or the--regionyou pass. If you belong to several organisations, pass--org-id(the command lists them). The generated database password is printed once; Garcon never needs it, so keep it in your password manager. To reuse a project you already have, pass--project-ref <ref>and this step is skipped. - Applies the table from the schema embedded in the binary (
garcon connect-supabase --print-sqlprints it) through the Management API (supabase db query --linked --project-ref …): no link step, no database password. - Fetches the secret key with
supabase projects api-keys --revealand pipes it, with the device name and the switch turned on, intoPUT /api/settings. The proxy probes the table with those credentials before storing anything.
Creating a project requires --create-project. To use an existing project and apply its schema, run garcon connect-supabase --project-ref YOUR_PROJECT_REF --name "work laptop". If creation succeeds but a later step fails, reuse the printed project ref when retrying so you do not create another project.
The --name defaults to the hostname and is this machine's label in the Device filter and the devices table. It is a label only: a hidden device id identifies the machine, so you can rename it any time in Settings, and every machine needs a distinct name.
The table
If you would rather not use the CLI, run this in the project's SQL editor (SQL Editor → New query → Run); garcon connect-supabase --print-sql prints the same text. It is idempotent: run it again after upgrading Garcon if Settings reports a missing column.
-- Garcon cross-device sync: one row per completion call, upserted by every device.
-- Idempotent: safe to run again after upgrading Garcon. Run it in the project's
-- SQL editor, or let `garcon connect-supabase` apply it (`--print-sql` prints it).
create table if not exists public.garcon_usage (
id text primary key, -- sha256 of device id + the row's fields
device_id text not null, -- random id generated once per machine
device text not null, -- the machine's display name
time bigint not null, -- unix milliseconds
harness text not null,
account text not null,
provider text not null,
model text not null,
status integer not null,
ms bigint not null,
queue_us bigint,
reused boolean,
connect_ms bigint,
dns_ms bigint,
tcp_ms bigint,
tls_ms bigint,
first_byte_ms bigint,
input bigint not null, -- uncached input tokens
cache_read bigint not null,
cache_write bigint not null,
output bigint not null,
synced_at timestamptz not null default now()
);
-- Add optional latency fields when upgrading an older Garcon table.
alter table public.garcon_usage add column if not exists queue_us bigint;
alter table public.garcon_usage add column if not exists reused boolean;
alter table public.garcon_usage add column if not exists connect_ms bigint;
alter table public.garcon_usage add column if not exists dns_ms bigint;
alter table public.garcon_usage add column if not exists tcp_ms bigint;
alter table public.garcon_usage add column if not exists tls_ms bigint;
alter table public.garcon_usage add column if not exists first_byte_ms bigint;
create index if not exists garcon_usage_synced_at on public.garcon_usage (synced_at, id);
create index if not exists garcon_usage_device_time on public.garcon_usage (device_id, time);
-- No policies on purpose: only the project's secret key (which bypasses row level
-- security) can read or write this table. The publishable key sees nothing.
alter table public.garcon_usage enable row level security;
Then in Settings → Sync: device name, project URL (https://<ref>.supabase.co, the ref is the 20-letter id in the project's dashboard URL), the secret key (Project Settings → API Keys → sb_secret_…), switch on, Save.
Other machines
Replace YOUR_PROJECT_REF in the commands below with your existing Supabase project ref, and replace the key-file path with a protected file containing its secret key.
Run garcon setup and configure this machine’s harness first. The project and table already exist, so joining only needs the URL and key. Choose a distinct device name. Do not copy another machine’s sync.json or data directory. The --skip-schema flag avoids database changes. Either rerun the command there with the CLI logged in:
garcon connect-supabase --name "home desktop" --project-ref YOUR_PROJECT_REF --skip-schemaor open Settings → Sync in that machine's dashboard, give it a different device name, paste the project URL and the secret key, switch sync on and Save. Without a browser, supply the key from a protected file or password manager through stdin. This needs no Supabase CLI and keeps the key out of command arguments and shell history:
garcon connect-supabase --name "home desktop" \
--project-url https://YOUR_PROJECT_REF.supabase.co --key-stdin < /path/to/protected-key-file
garcon doctorSave probes the table with the exact credentials first and refuses to store a configuration that does not work. After both machines have recorded usage and synced successfully, each one's dashboard lists the other, usually within about a minute under Settings → Sync and the Device filter appears in the filter bar.
Security and privacy
- What leaves a machine, per completion call: device name, time, harness, account, provider, model, HTTP status, latency figures and the four token counts. Never prompts, replies, API keys or session ids.
- Where the key lives:
~/.config/garcon/config.json, mode 0600, on each machine. The dashboard API never returns it (only whether one is stored) and it is sent only to the project URL.scripts/install.sh --uninstalldeletes the file. - Project access: use a dedicated project; a secret key can access other project data, not just Garcon’s table. Share it only with your own trusted machines.
- Which key: a new-format secret key (
sb_secret_…) or a legacyservice_roleJWT. Publishable and anon keys are rejected on Save: they get the anon role, and with RLS on they would silently see nothing. - The switch is absolute: off means no push, no pull, no probe. Turning it off keeps the settings; Forget key also removes the key.
- Local only: the proxy binds to loopback and the settings endpoint additionally requires a JSON content type and a loopback
Hostheader, so a web page cannot change them.
How it works
- Each row's
idis a hash of the machine's device id and the row's fields, so history backfills with stable ids and re-sending is a harmless upsert (Prefer: resolution=merge-duplicates). - Pushes go in batches of 500 with exponential backoff; a first enable backfills the whole local log. The push cursor is persisted so a restart never re-uploads.
- Pulls run every minute, ordered by the server-side
synced_at, with a five-minute overlap deduplicated by id so a slow commit is never missed. Other machines' rows are cached in~/.local/share/garcon/remote.jsonland shown even while offline. - Renaming a device is free: the id in
sync.jsonis what identifies the machine, and recreating the config never turns a machine into a "new" device.
Troubleshooting
garcon doctor and Settings → Sync show the latest push and pull errors. curl -s http://127.0.0.1:4141/api/settings shows the same as JSON.
- Invalid JWT
- The key is publishable, or a legacy key was pasted with a typo. Use the
sb_secret_key. - PGRST204 / could not find column
- The table is older than Garcon. Rerun
garcon connect-supabase --print-sqlin the SQL editor. It adds the optional latency columns used by this version. If an error remains, compare the named column with the schema before changing the table. - relation "garcon_usage" does not exist
- The SQL was not run, or ran in another project.
- pending never drains
- Read the push error; pushes retry with backoff up to five minutes and resume as soon as the cause is fixed. Flipping the switch off and on wakes the loop immediately.
- A machine appears twice
sync.jsonwas deleted, so it got a new device id and re-pushed its history under it.delete from public.garcon_usage where device_id = '<old id>'if it matters.- Rotate the key
- Create a new secret key in Supabase, paste it in Settings, Save, then revoke the old one.
- Start over
- Switch sync off, delete
remote.jsonl(the next pull rebuilds it), and drop the table if you no longer want the data in the cloud.