Quickstart
Postgres, Redis and MinIO in Docker; three Node processes; an account and a project you create in the browser. About five minutes, most of it pulling images.
1. Clone and install
git clone https://github.com/Adithya-Adi/syncline
cd syncline
pnpm installThe install runs prisma generate, which needs no database. Node 22 or newer, and pnpm — the version is pinned in package.json.
2. Configure
cp .env.example .envThe defaults match the Docker services. Two things to set yourself:BETTER_AUTH_SECRET, which signs session cookies, and nothing else — API keys are created in the app rather than in a file.
node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"Note the ports: 5442 for Postgres and 6399 for Redis, not the standard ones. If you already run either natively, the standard port is taken, and on some systems a host connection will silently reach your server instead of the container with nothing to say so.
3. Start the infrastructure
pnpm infra:up
pnpm db:migrate4. Run it
Three processes, three terminals:
# terminal 1 — ingest and read API on :4000
pnpm nx build api && node apps/api/dist/main.js
# terminal 2 — queue consumers
pnpm nx build worker && node apps/worker/dist/main.js
# terminal 3 — the web app on :3000
pnpm nx dev webThe API logs database connected and bucket "syncline" ready at startup. If it does not, fix that before going further — curl localhost:4000/v1/health names the failing dependency.
5. Create an account
Open http://localhost:3000/sign-up. Registration is open, and every account is provisioned its own organization — a new sign-up sees an empty dashboard, never anyone else’s recordings. To share a project, invite someone into your organization.
6. Create a project
A project owns a pair of API keys and the list of origins allowed to send recordings to it. Create one at /projects/new, listing the origins your app is served from.
- The public key (
pk_) ships in your browser bundle. It is public by design — the origin allowlist is what protects it. - The secret key (
sk_) is shown once, because only its hash is stored. It is for your OpenTelemetry exporter. Lost it? Rotate.
7. Record something
The project page shows this snippet with your real key already in it. The traceOrigins list decides which requests get a traceparent; it defaults to the page’s own origin.
import { startRecording } from 'syncline-browser';
startRecording({
key: 'pk_...',
endpoint: 'http://localhost:4000',
traceOrigins: ['http://localhost:3000'],
});The page’s origin has to be on the project’s allowlist, or ingest answers 403 naming the origin it rejected.
8. Watch it arrive
Open /dashboard. The first chunk lands within a few seconds of the page loading — the SDK flushes every five seconds or 64 KB, whichever comes first. Click a recording, then click a bar in any lane to zoom the timeline to that request.
Troubleshooting
| Symptom | Cause |
|---|---|
| Ingest returns 403 | The page origin is not on the project’s allowlist |
| Ingest returns 401 | Wrong key kind — recordings need pk_, OTLP needs sk_ |
| Recordings list is empty but ingest returned 202 | The worker is not running; the queue has the job waiting |
| Requests fail after adding the SDK | Your API must allow the traceparent header in Access-Control-Allow-Headers |
| New account sees no projects | Expected — each account gets its own organization. Ask an existing member for an invitation to theirs |
| Backend lane stays empty | Traces are arriving on a different trace id, or not arriving at all |