A private, encrypted place to keep track of your thoughts.
Website · Changelog · Documentation · MIT License
Diary is a journaling app built around one idea: what you write stays yours. Entry content is encrypted before it reaches the database, every read and write is scoped to the signed-in owner, and the writing surface is deliberately plain — a title, a page, and nothing asking for your attention.
- Distraction-free editor with per-entry font family and size, plus a blur toggle for writing in public.
- Autosave that debounces as you type and backs off when the network fails, so you never press save.
- Encrypted at rest — entry content is stored as AES-256-GCM ciphertext, so a database dump contains no readable text.
- Passwordless accounts through Better Auth and Resend, using six-digit email codes. Stripe powers the optional Plus plan.
- Light and dark themes, server-side rendering, and a mobile layout.
| Layer | Choice |
|---|---|
| Runtime and package manager | Bun |
| Monorepo tasks | Turborepo |
| Web | TanStack Start, React 19, TanStack Query, Tailwind CSS, shadcn/ui, CodeMirror |
| API | Elysia |
| Database | PostgreSQL with Drizzle ORM |
| Auth | Better Auth with Resend |
| Payments | Stripe |
| Validation | Zod contracts shared by both apps |
| Lint and format | Biome |
Diary is a Bun and TypeScript monorepo:
apps/web TanStack Start web application, server-rendered by Bun
apps/api Elysia HTTP API — the only writer of entry data
packages/contracts Zod request, response, and domain contracts shared by both apps
packages/database Drizzle schema, migrations, and the PostgreSQL connection
infra PostgreSQL image with the initial Diary schema
docs Full developer documentation
The browser talks to the API directly with a Better Auth session cookie; the web server renders UI and never proxies entry data.
- Bun 1.3.14 or newer
- PostgreSQL — use the Compose service in this repository or your own server
- A Resend API key and verified sending domain
- A Stripe account with one recurring Plus price
- Docker, if you want the Compose database or the full container stack
The API validates its database, Better Auth, Resend, encryption, and Stripe configuration at startup. Use test-mode Stripe credentials locally.
git clone https://github.com/kyledickey/diary.git
cd diary
bun installcp .env.example .envFill in BETTER_AUTH_SECRET, RESEND_API_KEY, ENCRYPTION_KEY, and the Stripe
values. Every variable is documented in
docs/configuration.md.
Keep all secret API values server-side in the API environment. Only VITE_*
values are exposed to the browser, and they are baked into the client bundle at
build time. The development scripts load the repository-root .env file for
both applications.
Bring up the Compose database, then apply all checked-in migrations:
docker compose up -d infra
bun run migratebun run migrate also works with an empty external PostgreSQL database. It
recognizes an older Compose schema and safely records its initial migration
before applying newer migrations.
Expose the local API with a tunnel, then point Stripe at
https://<tunnel-host>/api/auth/stripe/webhook. Copy the endpoint signing
secret into STRIPE_WEBHOOK_SECRET. Better Auth verifies the webhook and keeps
the local subscriptions table synchronized.
bun devThe web app runs at http://localhost:3000 and the API at
http://localhost:8080.
curl http://localhost:8080/health
# {"status":"ok","timestamp":"..."}Sign up at http://localhost:3000 using a one-time email code, then
create an entry and type into it — the header shows "Saving" and settles on
"Edited …". The generated API reference is at
http://localhost:8080/openapi.
bun dev # run both apps
bun dev:web # run only TanStack Start
bun dev:api # run only Elysia
bun check # typecheck every workspace
bun test # run the test suite
bun run build # build every workspace
bun lint # lint the repository
bun format # format the repositoryAfter building, run either production service directly:
bun --filter @diary/web start
bun --filter @diary/api startDatabase commands are routed through the database package and read DB_URL from
the environment:
bun run db:generate # generate SQL from the schema
bun run migrate # apply migrations
bun --filter @diary/database db:studio # browse the dataTests use bun:test and live beside the code as *.test.ts. Run the whole
suite with bun test, or a single file:
bun test apps/api/src/lib/cipher.test.tsBusiness rules live in the service layer and take interfaces, so tests exercise real logic against in-memory doubles with no database or network.
The repository has focused production images for each runtime:
infra/Dockerfile— PostgreSQL with the initial Diary schemaapps/api/Dockerfile— the bundled Elysia APIapps/web/Dockerfile— the TanStack Start SSR server and static assets
Run the complete stack through Compose:
cp .env.example .env
# Fill in the Better Auth, Resend, Stripe, encryption, and database values.
docker compose up -d infra
bun run migrate
docker compose up --buildThe web app is available at http://localhost:3000, the API at
http://localhost:8080, and PostgreSQL at 127.0.0.1:5432. Stop the stack
with docker compose down; add --volumes only when you intentionally want to
delete local database data.
Each image can also be built independently from the repository root:
docker build -f infra/Dockerfile -t diary-infra .
docker build -f apps/api/Dockerfile -t diary-api .
docker build \
-f apps/web/Dockerfile \
--build-arg VITE_API_URL=http://localhost:8080 \
-t diary-web .See docs/deployment.md for deploying the complete stack to Railway with managed PostgreSQL and separate web and API services.
Issues and pull requests are welcome.
- Branch from
main. - Make the change. Biome owns formatting and linting — four-space indentation,
100-column lines, double quotes — so run
bun formatrather than hand-tuning style. - Run
bun check && bun test && bun lintbefore opening a pull request. There is no CI workflow, so these checks are the gate. - Note anything user-facing in CHANGELOG.md.
A few conventions worth knowing before you start:
- Shared request and response shapes belong in
packages/contracts, so the web app and API cannot drift apart. - Keep business rules in the API's service layer, database access in
repositories, and scope every document query to
(id, owner_id). - Never route user input through the Markdown rendering used for policy pages.
docs/development.md has the full workflow, including recipes for adding an endpoint, changing the schema, and adding a UI component.
Full documentation lives in docs/:
| Guide | Covers |
|---|---|
| Getting started | First run, end to end |
| Architecture | Services, boundaries, runtime flows |
| Configuration | Every environment variable |
| HTTP API | Auth, documents, billing, errors |
| Data model | Schema, migrations, ciphertext format |
| Web application | Routes, data layer, editor |
| Development | Commands, tests, change recipes |
| Deployment | Docker, Compose, Railway |
| Security | Auth, ownership, encryption, secrets |
- Better Auth sessions are resolved from HTTP-only cookies at the API boundary.
- Every entry lookup and mutation is scoped to the authenticated owner.
- Entry content uses authenticated AES-256-GCM encryption.
- Stripe portal sessions are created from the authenticated user's stored customer ID; customer IDs are never trusted from browser input.
This is server-side encryption at rest, not end-to-end encryption, and
ENCRYPTION_KEY has no rotation path — see
docs/security.md for the full model and its limits.
Please report vulnerabilities privately to hi@kyle.so rather than opening a public issue.
See CHANGELOG.md for product history.
MIT © Kyle Dickey