Concepts
How Clivly reaches your app
Why the sync trigger URL is an address rather than a credential, and how it resolves in development and production.
Two directions, not one
Almost everything you configure travels one way — out of your app to Clivly. Exactly one thing travels the other way, and that difference explains nearly every "is this broken?" moment during setup.
Your app
app.example.com
- CLIVLY_SECRET_KEY
- heartbeat
- row pushes
Works from localhost
- CLIVLY_SYNC_TRIGGER_URL
- Run sync
- signed callback
Needs a public address
Clivly Cloud
api.clivly.com
The keys authenticate you. The trigger URL is an address. They are not peers, even though they sit next to each other in the same file: one proves who is calling, the other says where to call.
That is why a blank CLIVLY_SYNC_TRIGGER_URL is not a missing credential. Nothing can guess where your app will be deployed, so pairing cannot fill it in the way it fills in keys.
Your first pnpm dev
What a first run actually looks like on a laptop, so you can tell a normal stage from a real problem.
You have scaffolded an app, wired Drizzle and your auth, run clivly login, and started the dev server. Here is what is true at that moment:
- Dashboard shows your app live — works. The heartbeat is an outbound POST. It reaches Clivly from localhost like any other API call.
- Schema and contacts sync — works.
clivly push-schemaand the SDK's row pushes are outbound too. - Dashboard "Run sync" does nothing — expected. Clivly would have to dial your laptop, and
localhoston Clivly's servers means Clivly's own machine.
One button, not your whole setup. Nothing is misconfigured — the inbound leg simply has nowhere to land yet.
Local development: clivly dev
The fix for local development is a command, not an environment variable.
npx clivly devThis opens a cloudflared tunnel, builds <public origin>/api/clivly/tick, and registers it with Clivly as an override. You will see:
$ npx clivly dev
Tunnel: https://quiet-fox-42.trycloudflare.com/api/clivly/tick
Dashboard-triggered sync now points at this local session.Leave the variable blank in development
The override outranks whatever is in .env, so there is nothing to edit. On a clean exit (Ctrl-C) the override is removed again.
Development, deployed, pinned
The same variable across the three situations you will actually meet.
Local development
In .env
https://<yourDomain.com>/api/clivly/tick
clivly dev registers
https://quiet-fox.trycloudflare.com/api/clivly/tick
Deployed, no edit
In .env
(placeholder or deleted)
VERCEL_URL + syncTrigger.path
https://app-x7.vercel.app/api/clivly/tick
Pinned explicitly
In .env
https://app.example.com/api/clivly/tick
Used as written
https://app.example.com/api/clivly/tick
The placeholder never wins — angle brackets mark it unedited, so the SDK looks past it.
clivly login writes a visible template rather than an empty line, so the shape is obvious when you come to fill it in:
# Public URL of your tick route — replace <yourDomain.com> once deployed.
# Local dev: run `clivly dev` instead.
CLIVLY_SYNC_TRIGGER_URL=https://<yourDomain.com>/api/clivly/tickThe angle brackets are load-bearing. While they are there the SDK treats the line as unset, so an unedited placeholder never becomes a real — and broken — address.
Production: usually automatic
On most platforms you never set the URL by hand.
clivly init writes syncTrigger.path into your config. Combined with the origin your host already publishes, the SDK reports its own public URL on the first heartbeat after deploy — no edit required on Vercel, Cloudflare Pages, Railway, Render or Fly.
syncTrigger: {
// `path` lets the SDK build its own public URL from the hosting platform's
// environment, so on those platforms you never set the URL by hand.
path: "/api/clivly/tick",
url: process.env.CLIVLY_SYNC_TRIGGER_URL,
secret: process.env.CLIVLY_SYNC_TRIGGER_SECRET,
},Set CLIVLY_SYNC_TRIGGER_URL explicitly when you are behind a custom domain the platform does not advertise, on a host with no origin variable, or when you simply want it pinned.
Resolution order
Which address wins, in the order the SDK decides it.
Dashboard override
Set in Clivly, or registered by `clivly dev`. Beats everything.
syncTrigger.url
Your CLIVLY_SYNC_TRIGGER_URL — unless still a placeholder.
Platform origin + syncTrigger.path
VERCEL_URL, CF_PAGES_URL, RAILWAY_PUBLIC_DOMAIN, RENDER_EXTERNAL_URL, FLY_APP_NAME.
Observed origin
Learned from the first request the route serves.
clivly status prints whichever rung won.A stale tunnel override
The one failure mode worth recognising on sight.
The override registered by clivly dev outranks your production URL and is scoped to the workspace, not the environment. If clivly dev dies without a clean shutdown — a crash, a hard kill, a laptop sleeping — the dead trycloudflare.com address stays registered.
Your deployed app then has a perfectly correct CLIVLY_SYNC_TRIGGER_URL that is being ignored in favour of a tunnel that no longer exists, and nothing in your .env hints at it.
If Run sync fails in production and the env var looks right
Check for a stale override first — Integrations → Remote sync in the dashboard. Clearing it restores the reported URL.
Reading clivly status
What each stage means when the ladder prints.
Sync trigger URL
! Still the placeholder (https://<yourDomain.com>/api/clivly/tick)
— remote sync cannot call back into this app.
Replace <yourDomain.com> with your deployed origin, or delete the
line. For local development run `clivly dev`.A warning here is not a failure. Push-only deployments — where your app decides when to sync and nothing needs to call in — can leave this unset permanently.