Phase 2 tooling
Free-tier / OSS developer and ops tooling for the GraphOrg monorepo. Each tool has one job. Missing SaaS keys should no-op so localnpm run dev still works.
Anti-overlap: only the CI trio may share a job (GitHub Actions ↔ Jenkins ↔ Buildkite). Everything else is single-owner.
Neon/Postgres remains the app OLTP database.
CodeRabbit (AI PR review)
Role: reviews pull requests on GitHub. Does not run CI, host docs, or collect telemetry. Repo config:.coderabbit.yaml at the monorepo root (path filters, chill profile, path-specific guidance). CodeRabbit reads the YAML from the feature branch under review.
Free tier: free for public repos and limited private use for learning. No credit card required to start. Paid plans apply for heavier private-team usage — stay on free for scaffolding.
What you do (one-time install)
- Open CodeRabbit’s quickstart and sign up with your GitHub account (no credit card).
- Install / authorize the CodeRabbit GitHub App for the org or user that owns this repo.
- Grant access to the GraphOrg repository (all repos, or select this one only).
- Confirm the app appears under the repo’s Settings → Integrations → GitHub Apps (or Installed GitHub Apps).
- Open a PR (or push to an existing PR). Within a few minutes CodeRabbit should post a walkthrough / review comments. You can chat with
@coderabbitaion the PR.
- Comment
@coderabbitai configurationon a PR to see the resolved config (should pick up.coderabbit.yaml). - Prefer leaving
enable_free_tier: truein.coderabbit.yamlso free-tier features stay on for users not on a paid plan.
Mintlify (public docs site)
Role: browsable public documentation only. Not the product UI. Architecture ADRs stay in Linear; this site seeds runbooks fromdocs/ and links out to architecture.
Repo config: docs/docs.json plus MD/MDX pages in docs/ (index.mdx, architecture.mdx, ai-local-setup.md, tooling.md).
Free tier: Mintlify Starter (formerly Hobby) — free, no credit card, Git sync, custom domain options. Stay on Starter for scaffolding.
Local preview
From the repo root (Node 20.17+):cds into docs/ and runs npx mint@latest dev on port 3333 so it does not clash with apps/web on 3000. Open http://localhost:3333.
Or:
Publish (Starter)
- Go to mintlify.com/start and create a project (no card required).
- Connect this GitHub repo. In Git settings, enable docs.json is in a subdirectory and set the path to
/docs(no trailing slash). See Monorepo setup. - Install the Mintlify GitHub App so pushes to the connected branch deploy automatically.
- Use the project’s
*.mintlify.app/*.mintlify.siteURL for the live site; add a custom domain later if needed.
docs.json only.
GitHub Actions (primary CI)
Role: primary CI on push and pull request. Allowed overlap with Jenkins and Buildkite — those should run the same checks later. Do not remove this workflow when the other two land. Repo config:.github/workflows/ci.yml. Dependabot: .github/dependabot.yml (weekly PRs, not a required check).
Free tier: included with GitHub. Private repos have a monthly Actions minutes quota — three jobs (web, e2e, ai-worker) on every push adds up; the workflow cancels in-progress runs on the same ref.
Triggers: push to main and development, and every pull request.
Jobs (current gates)
CI does not set Sentry, PostHog, Resend, LaunchDarkly, or
OTEL_EXPORTER_OTLP_ENDPOINT. Those paths no-op without keys, and CI should keep proving that.
Placeholder env on the web / e2e jobs:
AUTH_TRUST_HOST=true and AUTH_URL=http://127.0.0.1:3000. Download the playwright-report artifact from a run to inspect failures.
Not gated yet (and why)
Not required: GitHub secrets for local SaaS keys. CI stays green without them.
Jenkins (local CI)
Role: the same checks as GitHub Actions, run on your machine. GitHub Actions stays primary. Buildkite (WEB-221) should mirror this set too. Repo config:Jenkinsfile (web → Playwright e2e → ai-worker pytest) and the Compose service built from infra/jenkins/Dockerfile.
Free tier: Jenkins OSS. You host it; the software is free.
Start
JENKINS_OPTS=--requestHeaderSize=262144) because cookies set by other apps on localhost are sent to this port too, and the 8KB default returns HTTP 431.
First launch:
- Unlock with the initial admin password:
- Install the suggested plugins and create the admin user.
- New Item → Pipeline → Definition: Pipeline script from SCM → Git. Repository URL:
https://github.com/ashal-s/graphorg.git, branch*/development, script pathJenkinsfile. - Build now. The run uses placeholder
DATABASE_URL/REDIS_URL/AUTH_SECRETand does not need Postgres, Redis, or any SaaS key. The Playwright HTML report is archived on the build.
Docker Desktop on Mac
Checks run inside the Jenkins container (Node 20, uv/Python 3.12, Playwright’s Chromium libraries are in the image). The Docker socket is not mounted. Do not switch this to Docker-in-Docker oragent { docker { ... } } on a Mac without reading this: Docker Desktop’s daemon lives in a Linux VM. /var/run/docker.sock on the Mac is a proxy into that VM. A sibling container can see host paths under /Users, but it cannot see /var/jenkins_home from inside the Jenkins container. Pipelines that docker run -v $WORKSPACE then fail with empty directories. That is why the socket stays off.
Not required for npm run dev or for GitHub Actions. Jenkins is optional local CI.
Buildkite (CI pipeline)
Role: the same checks as GitHub Actions and Jenkins. GitHub Actions stays primary. Do not remove the workflow or the Jenkinsfile when this pipeline exists. Repo config:.buildkite/pipeline.yml — web (format, lint, typecheck, Jest, build), Playwright e2e, ai-worker pytest. Placeholder DATABASE_URL / REDIS_URL / AUTH_SECRET. No SaaS keys.
Free tier: Buildkite Free — up to 5 users and 10 concurrent self-hosted jobs. A new account starts with a 30-day all-access trial and no credit card. After the trial, stay on Free. Jobs in this pipeline are meant to run on an agent on your machine, so they do not spend the hosted-agent vCPU minutes.
The agent needs Node.js 20+, Python 3.12 (python3.12 on PATH), and git. On Linux the e2e step also installs Chromium’s OS libraries. On a Mac it only downloads the browser.
What you do (one-time)
- Sign up at buildkite.com/signup. Create an organization. Skip hosted agents.
-
New pipeline. Connect the GitHub app and choose
ashal-s/graphorg. Leave the steps path as.buildkite/pipeline.yml. -
Set the pipeline’s default branch to
development(that is the branch that contains this file until it is onmain). - In the cluster sidebar open Agent Tokens (not Secrets) → New Token. Leave Allowed IP addresses empty. Copy the token from the creation screen. The list never shows it again. Keep it out of the repo.
-
Queues → New Queue. Key
self-hosted. Choose Self-hosted, not Hosted. The cluster’s existing default queue is hosted, and a Mac agent cannot join it. The pipeline targetsself-hosted. -
On your Mac, in a terminal you can leave open. Homebrew refuses the Buildkite tap until you trust that formula:
-
On the pipeline page, New Build, branch
development.
npm run dev or for GitHub Actions. Buildkite is optional local CI.
Better Stack (uptime heartbeats)
Role: external uptime checks only (“is the service alive?”). Not an app log drain — Pino / OpenTelemetry / Grafana own logs. Do not install a Better Stack (Logtail) logging SDK in the app. App endpoint:GET /api/health returns 200 with { "ok": true, "data": { "status": "ok" } }. It is unauthenticated (proxy.ts excludes /api from the auth redirect matcher). Optional DB ping is deferred.
Free tier: Better Stack free — personal projects, up to 10 monitors & heartbeats, ~3-minute check interval, email/Slack alerts. No credit card to start. Stay on free for scaffolding (do not enable log ingest for this lane).
What you do (one-time monitor)
- Sign up at betterstack.com (free).
- Open Uptime → Monitors → Create monitor.
- Set the URL to your deployed health endpoint, e.g.
https://graphorg.com/api/health(or a tunnel URL for local demos). - Keep HTTP status monitoring (expect 2xx). Optionally require the keyword
okin the body. - Choose email and/or Slack for alerts; create the monitor.
apps/web. The monitor only needs a public URL.
Playwright (browser E2E)
Role: browser end-to-end smoke tests only. Jest + RTL stay for unit/component tests (apps/web/tests/unit, tests/rtl).
Repo config: apps/web/playwright.config.ts + specs under apps/web/tests/e2e/. Chromium only for now.
Free tier: OSS (@playwright/test).
Local
/welcome and /login. The config starts next start with placeholder DATABASE_URL / REDIS_URL / AUTH_SECRET if unset (pages under test do not need a live DB).
HTML dashboard: every run writes apps/web/playwright-report/. Locally the report opens automatically on failure; otherwise run test:e2e:report. On CI, download the playwright-report artifact from the workflow run, then:
CI
Thee2e job in GitHub Actions installs Chromium, builds, runs npm run test:e2e, and uploads the HTML report artifact. See GitHub Actions for the full gate list.
The root Jenkinsfile and .buildkite/pipeline.yml run the same npm run test:e2e. Do not invent a second E2E suite.
Pino (structured app logging)
Role: in-process structured logger (info / warn / error with fields) for server code. Writes JSON to stdout/stderr. Not a SaaS product and not a Better Stack / Sentry / Logtail drain — those tools keep their own lanes (uptime, exceptions). OpenTelemetry may bridge logs later (WEB-223).
Repo config: apps/web/src/lib/logger.ts. Used from auth failures, AI rate-limit fail-open, and instrumentation.ts env validation. Client components (e.g. error.tsx) keep console.*.
Free tier: OSS (pino, pino-pretty in development).
Usage
env.ts when set):
Defaults:
debug outside production, info in production. Development uses pino-pretty; production and tests emit JSON lines.
Not required: any external account or API key. Do not install a Better Stack logging SDK for this lane.
Resend (transactional email)
Role: transactional email only (member-added notice, future invites). Not a marketing platform and not the auth provider (Auth.js + credentials stay). Repo config:- Mailer:
apps/web/src/lib/email/resend.ts—sendEmail/notifyMemberAddedno-op whenRESEND_API_KEYis unset. - Templates (React Email):
apps/web/emails/—member-added,welcome,invite.
POST /api/users) renders member-added and sends it (never includes the password). welcome / invite are scaffolded for preview; invite is not wired to send yet.
Free tier: Resend free monthly send quota. No credit card to start. Stay on free for scaffolding.
Preview templates (React Email dashboard)
What you do (one-time send setup)
- Sign up at resend.com and create an API key.
- Add to
apps/web/.env.local:
- Smoke-test (until a domain is verified, use
onboarding@resend.devas from, and send only to your Resend account email ordelivered@resend.dev):
- For production, verify a domain and set
EMAIL_FROMto an address on that domain.
Sentry (exceptions only)
Role: application exceptions / error events only (“is something broken?”). Not APM / performance (OpenTelemetry → Grafana), not session replay (PostHog), not uptime (Better Stack). Repo config:- Init:
apps/web/src/instrumentation-client.ts,sentry.server.config.ts,sentry.edge.config.tsviainstrumentation.ts - Capture:
app/error.tsx,app/global-error.tsx,onRequestError - Build:
withSentryConfiginnext.config.ts(source maps upload only whenSENTRY_AUTH_TOKENis set) - Shared options:
apps/web/src/lib/sentry/options.ts— notracesSampleRate, noreplayIntegration
What you do (one-time)
- Create a Sentry project (Next.js) in your org.
- Add to
apps/web/.env.local:
- Optional for readable production stack traces (CI / Vercel):
- Restart
npm run dev, trigger an error in the UI (or throw from a route), and confirm it appears under Issues in Sentry.
PostHog (analytics + session replay)
Role: product analytics and session replay only (“what did users do?”). Not exceptions (Sentry), not uptime (Better Stack), not feature flags (LaunchDarkly — WEB-220). PostHog flag evaluation is disabled in the SDK. Repo config:- Init:
apps/web/src/instrumentation-client.tsviaposthog-js(same file as Sentry client) - Identify:
PostHogIdentifyinAppShell—userId+ org group only (no email) - Reset on logout in
UserMenu - Shared options:
apps/web/src/lib/posthog/config.ts—advanced_disable_feature_flags, session replay on withmaskAllInputs
What you do (one-time)
- Create a PostHog project (US or EU cloud).
- Add to
apps/web/.env.local:
- In the PostHog project settings, leave feature flags unused (or ignore them) — the SDK opts out with
advanced_disable_feature_flags. Enable session replay if the project wizard asks. - Restart
npm run dev, sign in, click around, and confirm events / a replay appear in PostHog.
LaunchDarkly (feature flags)
Role: feature flags only (“is this feature on for this user/org?”). Not analytics (PostHog), not CI, not exceptions (Sentry). PostHog flag evaluation stays disabled in the JS SDK. Repo config:- Server SDK:
@launchdarkly/node-server-sdkviaapps/web/src/lib/flags/launchdarkly.ts - Example flag:
ai-chat-enabled— gates Ask AI UI (AiChatGate) andPOST /api/ai/chat(defaulttruewhen LD is unset) - Optional
NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_IDreserved for a future client SDK (not required for this ticket)
What you do (one-time)
- Create a LaunchDarkly account / project (Developer plan).
- Create a boolean flag with key
ai-chat-enabled(default on is fine). - Copy the server-side SDK key (Settings → Projects → … → SDK keys — not the client-side ID).
- Add to
apps/web/.env.local:
- Restart
npm run dev. Toggle the flag in the LD dashboard and confirm Ask AI appears/disappears after a refresh (and that/api/ai/chatreturns 403 when off).
variation(...) returns the caller’s default (ai-chat-enabled → true).
OpenTelemetry (traces + metrics)
Role: instrumentation standard — emit traces and metrics fromapps/web and services/ai-worker over OTLP. Not a UI (Grafana — WEB-224), not exceptions (Sentry), not uptime (Better Stack), not session replay (PostHog).
Repo config:
- Web:
NodeSDKinapps/web/src/instrumentation.node.ts, loaded frominstrumentation.tswhenOTEL_EXPORTER_OTLP_ENDPOINTis set. It records Node runtime metrics (nodejs.eventloop.utilization,v8js.memory.heap.used) and HTTP request duration (http.server.request.duration) and exports them to Prometheus via the collector. - AI worker:
services/ai-worker/src/ai_worker/otel.py+ FastAPI instrumentation - Trace context:
POST /api/ai/chatinjects W3Ctraceparenton the upstream worker fetch - Collector: Compose service
otel-collector(ports 4317 gRPC / 4318 HTTP) with config atinfra/observability/otel-collector-config.yaml— forwards to Tempo + Prometheus (see Grafana below) - Pino → OTel log bridge: not wired yet (optional follow-up)
What you do (local)
- Start the observability stack (or full Compose):
- Add to
apps/web/.env.local(andservices/ai-worker/.envif you run the worker):
- Restart
npm run devand (if used) the AI worker. Hit a few pages / Ask AI. - Open Grafana at http://localhost:3001 (anonymous Viewer; sign in as admin/admin to explore) — see the Grafana section below.
Grafana (OTel dashboards)
Role: dashboards / Explore UI on OpenTelemetry system telemetry (Tempo traces + Prometheus metrics) — “how is the system performing?”. Not uptime (Better Stack), not exceptions (Sentry), not session replay / product analytics (PostHog). Repo config:- Compose:
tempo,prometheus,grafana,otel-collectorindocker-compose.yml - Provisioning + starter dashboard under
infra/observability/
Free tier: Grafana OSS + Tempo + Prometheus in Docker Compose — fully free, no cloud account.
What you do (local)
docker compose up -d otel-collector tempo prometheus grafana- Ensure
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318and restart the app; click around. - Open http://localhost:3001. The port is bound to localhost. Anonymous visitors are Viewers and can open dashboards. Sign in as admin/admin for Explore and admin actions.
- Confirm Connections → Data sources lists Prometheus and Tempo.
- Open dashboard GraphOrg → GraphOrg OTel overview (or Dashboards → search
OTel). - Explore → Tempo → search service
graphorg-webfor traces.
npm run dev: Grafana stack. Apps boot without OTel; Grafana is optional local tooling.
Databricks Free Edition (analytics export)
Role: offline analytics on exported events. A learning scaffold for the lakehouse. Not the app database — Neon/Postgres stays the source of truth for people, teams, and the rest of the org graph. Not product analytics (PostHog) and not the commercial Databricks platform deferred to Post-MVP. Repo config:- Event shapes:
analytics/events.schema.md - Sample writer:
npm run analytics:export --workspace=apps/web→ gitignoredanalytics/exports/events.jsonl - Notebook:
analytics/databricks/01_explore_exports.py - Steps:
analytics/README.md
What you do (one-time)
- Sign up at databricks.com/learn/free-edition with Google, Microsoft, or an email code.
-
Generate the sample (no database, no API key):
-
In the workspace, Catalog → default schema → Create volume named
graphorg. -
New → Add or upload data → Upload files to a volume, and upload
analytics/exports/events.jsonl. Free Edition will not let the notebook download that file. -
Copy the volume path (
/Volumes/<catalog>/default/graphorg/events.jsonl). -
Workspace → Import
analytics/databricks/01_explore_exports.py. -
Paste the path into
EVENTS_PATH, attach serverless compute, and Run all.
page_view events and 3 ai_chat_completed events. No emails, names, or photo URLs.
Not required for npm run dev. Nothing in the app calls Databricks.