Notes is an open-source capture app for ideas, todos, reminders, and bookmarks. The web UI is built with TanStack Start (React) and talks to a Convex backend. Authentication uses Better Auth: anonymous sessions work immediately; Google OAuth is supported when you configure it.
- Fast capture without leaving your flow
- Convex-backed persistence with real-time sync
- Optional Google sign-in alongside anonymous use
- Frontend: TanStack Start, React 19, Vite, Tailwind CSS
- Backend: Convex (
convex/) with Better Auth via kitcn auth helpers - Tooling: Biome (via Ultracite), TypeScript, Vitest
Install dependencies and start Convex plus the Vite dev server (port 3000):
pnpm install
pnpm devConfigure the Convex deployment and required secrets:
pnpm exec convex dev
pnpm exec convex env set BETTER_AUTH_SECRET "<generate-a-long-random-secret>"
pnpm exec convex env set SITE_URL "http://localhost:3000"Optional — Google sign-in (set both in Convex; see .env.example for local reference):
pnpm exec convex env set GOOGLE_CLIENT_ID "<your-client-id>"
pnpm exec convex env set GOOGLE_CLIENT_SECRET "<your-client-secret>"Copy .env.example to .env.local (or your preferred env file) and fill at least:
| Variable | Purpose |
|---|---|
VITE_CONVEX_URL |
Convex deployment URL (.cloud) |
VITE_CONVEX_SITE_URL |
Convex HTTP site URL (.site) for the auth proxy |
VITE_SITE_URL |
Public origin of the web app (must match SITE_URL in dev) |
CONVEX_DEPLOYMENT |
Used by Convex CLI when configured |
Better Auth creates anonymous users by default: new visitors get a session without signing in. Captures are tied to a users row; users.authId stores the Better Auth user id for account linking later.
pnpm buildNitro emits a Node server preset under .output/:
- Server entry:
.output/server/index.mjs - Static assets:
.output/public/
Smoke-test locally from .output:
node ./server/index.mjsUse the same VITE_* values at build time so they are baked into the client bundle.
Self-hosting means you run the web app on hardware or a cloud you control, while backend data and auth live in a Convex deployment you own. This repository does not ship a standalone database in a container; Convex is the hosted backend.
-
Install the CLI and log in:
pnpm exec convex login -
From this repo:
pnpm exec convex deploy(choose or create a production deployment when prompted) -
In the Convex dashboard for that deployment, set environment variables:
Name Notes BETTER_AUTH_SECRETLong, random secret SITE_URLExact public origin of your site (e.g. https://notes.example.com). Avoid mixing trailing-slash styles between here and the app.GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRETOnly if you use Google OAuth
SITE_URL must match the URL browsers use to open the app. It feeds Better Auth baseURL, trustedOrigins, and related settings.
Set build-time variables in CI, Docker build args, or a Vite-loaded env file (e.g. .env.production.local):
| Variable | Value |
|---|---|
VITE_CONVEX_URL |
Your Convex deployment URL (ends with .convex.cloud) |
VITE_CONVEX_SITE_URL |
Convex HTTP / site URL (ends with .convex.site) |
VITE_SITE_URL |
Same origin as Convex SITE_URL |
Then:
pnpm install --frozen-lockfile
pnpm buildShip the contents of .output/ to your host (rsync, object storage + compute, or a container image). Start the server with the process working directory at .output:
node server/index.mjsPut a reverse proxy (Caddy, nginx, Traefik, etc.) in front for TLS; forward to the Node process and set PORT (or your platform’s port binding) as required by Nitro/Node.
In Google Cloud Console, set Authorized JavaScript origins and redirect URIs to match Better Auth and your Convex HTTP routes for your real SITE_URL. Wrong redirect URLs are the most common cause of “works locally, fails in prod” after self-hosting the UI.
- Run
pnpm exec convex deploywhen you changeconvex/*. - Rebuild and redeploy the web app when frontend code changes or any
VITE_*value changes.
Built by N6 Studio.
From the repo root:
pnpm format
pnpm lint
pnpm typecheck
pnpm test
pnpm build