I'm a full-stack developer whose best feature is shipping anyway.
Meet Rae Calder, a developer who treats side projects the way a food critic treats menus: everything gets tried at least once. Rae builds clean, boring-in-a-good-way web software, holds strong opinions about database indexes, and keeps a paper notebook full of bugs that solved themselves on the walk to the kettle.
Building Emberpad and Querybird whenever the day job looks the other way.
Teams I have shipped with
Tessmoor
Plovera
Tarnhaul
Halcyra
Pellbrook Co-op
Sorrelow
Selected work
Four projects, four kinds of work: a product I run, an open-source tool, a client build and an in-house migration.
The hardware, software and paper behind the work. Nothing here is sponsored, and most of it is older than it looks.
In the studio in Portland, on the one sunny afternoon in February.
I'm Rae. I make software that stays out of the way.
I have spent eleven years building the parts of products that people only notice when they break: billing, sync, deploys and the internal tools that hold a team's week together.
I started at Tarnhaul building route dashboards for freight dispatchers, moved to Seattle to work on a real-time editor at Plovera, and spent four years at Tessmoor untangling a billing system on purpose. In 2025 I went independent. I now work with one team at a time on contracts of three to six months, and I spend the rest of my week on Emberpad and Querybird.
I like problems where correctness matters and the solution should be boring: money, data that has to survive, systems that page someone at night. I write things down before I build them, and I write about them afterwards, mostly on the writing page.
Small surface, long lifeFewer features, fewer dependencies, fewer moving parts. Everything I ship should be easy to delete.
Write it down firstA one-page proposal before any week-long task. It is cheaper to be wrong in a document than in production.
Boring on purposePostgres, server-rendered HTML, one binary. Excitement belongs in the product, not the stack.
A changelog of the career so far, newest first. One joke per year, maximum.
Outside the editor
Most weekends I am on a trail in the Gorge, where the cliffs are made of the same basalt this site is named after. Columnar basalt forms when lava cools slowly and cracks into regular columns, which is roughly my approach to software.
I also run a twelve-person book club that meets on the first Thursday of the month, and I keep a paper notebook of bugs that fixed themselves on the walk to the kettle. It is thicker than I would like.
Résumé
The one-page version. For the stories behind these lines, read the case studies.
Get in touch
Tell me about the project, the team and the deadline. I read everything and reply within two working days.
Next opening: November 2026One contract at a time, three to five days a week
Pacific timeOverlaps with US mornings and European afternoons
Best fitBilling and payments, real-time sync, internal tools, performance
Emberpad launched without a settings page, without password reset and without a pricing page. It had twelve users by the end of the first weekend, and every one of them told me something I would not have guessed.
I have started more side projects than I have finished, and the unfinished ones all died the same way: in the gap between "works on my machine" and "good enough to show someone". That gap is not technical. It is a list of things I believe a real product needs, and the list grows faster than I can work through it.
Perfection is a queue that never drains
Every item on a pre-launch list is reasonable on its own. Dark mode. An onboarding tour. Export to PDF. The problem is that the list is written by the person least qualified to prioritize it: me, before anyone has used the thing.
So I stopped keeping that list. I keep a much shorter one that answers a different question: what would stop the first ten users from using this at all?
The weekend checklist
This is the whole list. If a project passes it, it goes public on Sunday evening.
One path works end to end. A new person can arrive, do the main thing, and leave with the result. Not three paths. One.
Data survives. Backups run, and I have restored one. Losing someone's work is the only mistake that ends a project on day one.
I can be reached. A plain email address on every page. Not a form, not a chat widget.
It says what it is not. A short "not yet" list on the homepage sets expectations better than any amount of polish.
Password reset did not make the list. For the first month, "I forgot my password" was an email to me, and I reset it by hand. It happened four times.
What launching early taught me
The first twelve users ignored the feature I was proudest of, real-time cursors, and asked for something I had never considered: a way to mark a document as decided. That request became decision blocks, which became the reason teams pay for Emberpad. The full story is in the case study.
The feature list you write before launch is a guess. The one users write after launch is a map.
I would not have found that in another month of polishing. I would have found a nicer settings page.
The honest cost
Shipping early is not free. Some people saw a rough product and never came back, and a few bugs were embarrassing. But a rough product with users gets better every week, and a polished product without users only gets older.
If a project is sitting in a private repository right now, run it through the four questions above. If it passes, the next step is not another feature. It is a link.
Every explanation of database indexes I read early on started with B-trees. The one that finally worked for me started with a library card catalog, and it made composite indexes obvious in a way the diagrams never did.
A table is a pile of books
Picture a library where books are shelved in the order they arrived. That is a table without indexes. To find every book by one author, you walk every shelf. In database terms that is a sequential scan, and on a large table it is why a query that took 3 ms in development takes four seconds in production.
A card catalog is a separate, sorted drawer of cards. Each card holds one value, say the author's name, plus the shelf location of the book. Finding an author means flipping to the right letter, reading a few cards and walking straight to the right shelves. That drawer is an index.
Composite indexes are cards sorted twice
Now imagine the drawer is sorted by author, and within each author by year. That is an index on (author, year). It answers these questions quickly:
Every book by Okafor.
Every book by Okafor from 2019.
Books by Okafor after 2015, in order.
It cannot help with "every book from 2019", because the 2019 cards are scattered across every author. This is the column-order rule, and the catalog makes it intuitive: an index helps with its leftmost columns first.
books.sql
-- Fast: filters on the leftmost columnSELECT * FROM books
WHERE author = 'Okafor'AND year > 2015;
-- Slow: skips the leftmost columnSELECT * FROM books WHERE year = 2019;
CREATE INDEX books_author_year ON books (author, year);
Three checks before adding another index
Every index is another drawer to update whenever a book arrives. Writes get slower and storage grows. Before adding one, I run three checks.
1. Is the planner already using what exists?
Run the query with EXPLAIN ANALYZE and read the plan from the most indented line outward. Compare estimated rows with actual rows. If they differ by ten times or more, the statistics are stale, and ANALYZE may fix the query without a new index.
2. Which indexes are never read?
unused-indexes.sql
SELECT relname, indexrelname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0ORDER BYpg_relation_size(indexrelid) DESC;
Unused indexes cost writes and buy nothing. On one Tessmoor table, dropping three of them cut insert time by a third.
3. Would one index serve two queries?
Indexes on (a) and (a, b) are usually one index too many. The second answers every question the first one does.
Want to practice on a real schema? The query plans workshop uses a deliberately broken database, and Querybird will draw its tables for you.
The short version
An index is a sorted copy of some columns plus a pointer back to the row. Sorted by more than one column, it helps with the leftmost ones first. Everything else, including B-trees, is detail you can learn when you need it.
During the Tessmoor migration we moved 38,412 subscriptions to a new billing model and charged every one of them at least once afterwards. None of them was charged twice. That was not luck. It was one design decision we made in the first week.
Retries are free until they are not
Every distributed system retries. A request times out, a worker restarts, a queue redelivers. For most operations a retry is harmless. For a charge, a retry is a support ticket, a refund and a customer who now reads every invoice closely.
The standard answer is an idempotency key: a value sent with a request so the server can recognize a repeat and return the original result instead of doing the work again. The hard part is not the mechanism. It is deciding what the key should be.
Derive the key from facts, not requests
Our first design used a random ID generated per request. It protected against network retries and nothing else. If a worker crashed after charging but before recording the result, the job restarted, generated a new ID and charged again.
The fix was to derive the key from the business facts that make a charge unique:
charge-key.ts
// One charge per account, per period, per invoice version.export functionchargeKey(invoice: Invoice): string {
return [
'charge',
invoice.accountId,
invoice.periodStart.toISOString().slice(0, 10),
`v${invoice.version}`,
].join(':');
}
// charge:acct_8841:2024-02-01:v3
Now any process that tries to charge the same invoice, for any reason, produces the same key. The payment provider and our own ledger both reject the second attempt.
Versions make corrections safe
Invoices change. A customer reports usage late, or finance applies a credit. We never mutate an invoice; we issue a new version. A new version gets a new key, so a legitimate correction goes through while an accidental repeat does not.
This one rule removed a whole category of meetings. When finance asked whether something could have charged twice, the answer was a key they could look up, not an argument.
Store the key before calling out
The order of operations matters more than the key format:
Insert the key into our ledger with status pending, inside a transaction. A unique constraint rejects duplicates.
Call the payment provider with the same key.
Update the ledger row with the result.
If the process dies between steps two and three, the next attempt finds the pending row, asks the provider for the result of that key and finishes the job. Nothing is charged twice and nothing is lost.
Shadow invoices caught what keys could not
Idempotency stops you from charging twice. It does not stop you from charging the wrong amount once. For that, we ran the new engine next to the old one for 90 days and compared 115,236 invoices. The details are in the Tessmoor Billing case study.
Money is not eventually consistent. Design as if every retry has a real customer watching.
Cache-Control has more directives than anyone needs. After two incidents caused by caching the wrong thing, I wrote down the six that matter and the four recipes I now use on every project.
The six directives you actually need
max-ageHow long the browser may reuse a response without asking.
s-maxageThe same, but for shared caches like the edge. Overrides max-age there.
stale-while-revalidateServe the old copy instantly while fetching a fresh one in the background.
privateOnly the browser may cache this. Never the edge.
no-storeNobody caches this, anywhere, ever.
immutableThis URL will never change. Do not even revalidate.
Four recipes
headers.txt
# Fingerprinted assets: cache forever
Cache-Control: public, max-age=31536000, immutable# HTML: short at the edge, refresh in the background
Cache-Control: public, max-age=0, s-maxage=60, stale-while-revalidate=600# Signed-in API responses: browser only
Cache-Control: private, max-age=0, must-revalidate
# Anything with a secret in it
Cache-Control: no-store
The two incidents
The first was a signed-in dashboard served with public, s-maxage=300. For five minutes, the edge served one customer's account page to anyone who requested the same URL. The fix was private, plus a test that fails the build if any authenticated route sends public.
The second was the opposite: a pricing page with no-store, copied from an old snippet. It was fine until a launch, when every visit hit the origin and checkout p95 tripled. Adding s-maxage=60 with stale-while-revalidate took the load off within a minute.
Keep cache headers next to the route that sends them, not in a global config file. When someone changes the route, they see the header.
A pre-deploy check
Every authenticated response is private or no-store.
Every fingerprinted asset is immutable.
HTML has a short s-maxage and a longer stale-while-revalidate.
You can purge the edge cache in one command, and you have done it once on purpose.
This site has been rewritten five times. The first four rewrites each took a month and added a tool. The fifth took two weekends and removed all of them.
Four rewrites, briefly
2016: a static site generator with a theme I spent more time configuring than writing for.
2018: a single-page app, because I was learning one at work. Posts took four seconds to appear on a phone.
2020: a framework with server rendering and a content pipeline. Fast, but a dependency update broke the build every few months.
2022: the same framework, rewritten for its new router. I published one post that year.
What I kept
Plain HTML pages, one stylesheet with design tokens at the top, and posts as files. A small script turns markdown into HTML and writes the feed. It is 180 lines long and has not changed in a year.
What I deleted
The client framework, the build step, the image pipeline, the analytics script, the comment widget and about 1,400 dependencies. Nothing I deleted has been missed, by me or, as far as I can tell, by anyone else.
The cheapest rewrite is the one that leaves less to rewrite next time.
The only rule now is that the site must be editable from any computer with a text editor and deployable by copying files. It has been easy to write for ever since, which was the point all along.