Below you will find pages that utilize the taxonomy term “Side Projects”
20 Primitive Project Ideas: Tiny Go Engines That Do the Essential 10% of Redis, Kafka and Etcd
Given the direction of AltSql, BareProxy, Precomputing and the VPN primitives work, there is still a surprisingly large design space for projects that are smaller than platforms but more fundamental than ordinary applications. The best candidates have a very simple sentence behind them: “X currently requires a framework, service or stack; this does the essential 10% in one tiny binary, library or file.”
Look for primitives that can begin as a credible 2,000–10,000-line Go project, have an immediately understandable demo, and later sit underneath commercial products rather than needing to become a SaaS business immediately. Here are twenty directions that fit that model.
40 Projects Worth Building: SQLite for X, Tiny Infrastructure, MCP Tools and Edge Systems
The most promising territory for a small team isn’t another large framework. It’s the opposite: infrastructure software you can explain in one sentence, ship as one binary or embed as one library, run with almost no operational burden, and that solves one irritating problem unusually well. SQLite and nginx are the right mental models because their appeal is architectural, not just functional: small surface area, predictable behavior, few dependencies, easy deployment, and usefulness far beyond the original use case. (More on that in the SQLite and nginx pattern as a product strategy.)
40 Radically Different Infrastructure Primitives, From Reach and Expect to Ignorance and Stop
This list deliberately leaves “tiny version of Redis, Kafka or SQLite” territory behind. That was the first list of 20 primitives. The more interesting search space is a primitive that introduces a new operation, one developers might eventually feel should have existed all along. Some research paper or niche project has probably explored pieces of these. They’re meant as category-level concepts, not smaller copies of familiar products.
Forty primitives
1. Reach. Store a current state plus transition rules and continuously maintain which states are reachable from here: REACHABLE(current_state). A database answers what is; Reach answers what can happen. Uses: AI agent safety, workflows, infrastructure, manufacturing, cybersecurity attack paths.
Counterfactual Store vs. Markov Chains: From Branching State to Reach, an Engine for Possible Futures
The Counterfactual Store does resemble a Markov chain at the conceptual level. Both deal with states and the possible transitions between them. But a Markov chain is mainly a mathematical model for probabilistic transitions: given that I’m in state A, what’s the probability the next state is B, C or D? And the classic Markov property says the next-state probability depends on the current state, not the full history.
The Counterfactual Store is deterministic branching, not probabilistic transition. If the current application state is A, you create a hypothetical A′, change something, propagate the consequences, inspect the result and throw it away without touching A. No probability needs to be involved anywhere.
Counterfactual Store: A Database That Stores Alternatives, Not Just Facts
One direction that feels genuinely different from the earlier list of 20 primitives is a Counterfactual Store. Adjacent ideas certainly exist. Provenance databases, event sourcing, probabilistic databases, simulation systems and version control all touch pieces of it. But as a deliberately tiny infrastructure primitive, there’s no established category built around this exact abstraction.
Normal databases answer “what is true?” Temporal databases, and Temporal KV, add “what was true?” A Counterfactual Store adds a third basic question: what would be true if X were different?
Screening 20 Infrastructure Primitives Money First: Why Distill and Precomputing Survive
Run the 20 primitive project ideas through the four-question screen strictly and the ranking changes a lot. Several technically attractive ideas turn into weak projects. The economic buyer is unclear, free infrastructure already solves the problem well enough, or the moat amounts to “ours is smaller.”
So Statefile, Lease, BareQueue, TTL, Count, Remember, Freeze and probably ConfigLog go out at this stage. They all solve real engineering problems, but the money question is poor. Redis, SQLite, existing Go libraries, message queues, object storage, Git and cloud infrastructure already give acceptable answers. A tiny implementation might make a respectable GitHub project. “Simpler and smaller” doesn’t tell you who eventually writes a meaningful check.
Temporal KV: A Key-Value Store That Remembers History
Temporal KV is a key-value database where a key doesn’t just have a current value. The database automatically remembers how that value changed over time.
In an ordinary KV store, you might write price = 100, then later price = 110, then price = 125. Ask for price and you get 125. The earlier values have normally been overwritten. In a Temporal KV store, all three states stay logically accessible. You can ask for the current value and get 125, or ask “what was price at 10:00 yesterday?” and get 100. The basic abstraction stops being key → value. It becomes key + time → value.
A Background Job Queue in One SQLite File Covers What Most Apps Deploy Redis For
A signup handler has to send a welcome email, and sending it inline puts a third-party API call inside your request. So you move it into a background job, and the usual next step is Redis, a client library, a worker process, a dashboard, and a note in the deploy docs about what happens to queued jobs when Redis restarts without persistence. If the app runs on one machine, you’ve added a second stateful service to look after a table’s worth of data.
A Database Where Every Value Remembers Its Source, Confidence and Extraction Time
Somebody pastes a population figure into a shared spreadsheet: 1,234,567. It might have come from a census table, a Wikipedia infobox, a city press release or a model’s summary of one of those, and three weeks later nobody can say which. The paste kept the digits. Everything that made them believable stayed in a browser tab that closed long ago.
A value without provenance is a rumor with a data type.
A Feature Store Small Enough to Embed: Point-in-Time Reads for Local and Edge ML
A churn model looks excellent in the notebook and mediocre in production. The model isn’t broken. The training table computed orders_7d with a rolling window over the full history, so some rows counted orders placed after the label date. The serving code, written later in another language, counts a calendar week instead of seven days. One feature, two definitions, and a quiet look into the future. Those are the two bugs a feature store exists to prevent: training-serving skew and label leakage.
A Schema Diff That Warns Which Migration Will Lock a 40-Million-Row Table or Lose Data
The diff in the pull request is one line: ALTER TABLE orders ALTER COLUMN total TYPE bigint;. It reads like a rounding fix, so the reviewer approves it, and the SQL is correct. On a table with 41 million rows it’s also a full rewrite under a lock that stops every read and write until it finishes. A diff shows what changes, never what the change costs.
A schema diff worth using would print consequences instead of SQL, in sentences like “this rewrites a 41-million-row table and blocks every read and write” or “the rollback is impossible without a backup”. Those sentences depend on three things: the statement, the engine and its version, and the size of the table it lands on. With all three you can post a risk report on every migration pull request. With only the first you have a linter.
A Single-File Log Database: Pipe Logs In, Query Them With SQL, Hand the File to Anyone
The incident is over, the postmortem is Thursday, and someone wants the logs from the bad three hours. You can attach a gzip nobody will open, paste a screenshot of a grep, or stand up a search cluster to answer one question. The question is usually “which paths returned 500, and when did it start”, which has a GROUP BY hiding in it, and a GROUP BY is where grep runs out. A full observability stack answers it, but that’s a heavy answer for a team that doesn’t already run one.
A Telemetry Database That Forgets on Purpose Keeps Rollups and Anomalies Instead of Raw Rows
At 300 requests a second, one latency metric adds about 26 million rows a day. Nobody will read most of them again. What people ask for a month later is the p99 per route for last Tuesday, and the one request that took nine seconds. A rule that deletes rows after 30 days throws away both answers. Keeping every row forever pays storage bills for data nobody reads.
The telemetry store worth building decides at write time what has to survive. It folds everything else into coarser buckets on a schedule and keeps the few raw events that carry information, whole. Forgetting becomes part of the schema, declared in a policy, instead of a cron job that deletes by age. This is the argument for reducing data near the source, applied to time series.
A Tiny SQL Engine for JSON Streams Fits Between jq and a Stream Processing Cluster
Your service writes one JSON object per line to stdout, and you want the average cpu for host web-3 over the last five minutes. jq can’t answer that, because it forgets each object once it has printed it and has no idea what “five minutes” means. You can fake both with jq -n 'reduce inputs as $e (...)' plus a timestamp comparison, and you’ll have a script nobody wants to touch twice. DuckDB answers the question in one line, but only against a file as it exists right now. ksqlDB, Materialize, RisingWave, Arroyo and Flink SQL answer it continuously, once you’ve deployed a service and agreed to operate it.
An Embedded Sketch Database: Billions of Events, Megabytes of Storage, Error Bars on Every Answer
Counting unique visitors exactly means remembering every visitor. Say 6 million distinct IPv4 addresses hit a checkout service today. Packed at 4 bytes each, that set is 24 MB before any hash table overhead, and you need one per service, per region, per day you want to compare. Redis answers the same question with a HyperLogLog in about 12 KB per key, at a standard error of about 0.81%.
The thin part is the database around it: a SQLite-shaped file of sketches with time buckets, rollups and a SQL front door (check before you start; this space moves). The version worth building stores nothing but mergeable sketches, grouped by dimension and time bucket, and every aggregate returns its error bound next to the value. You never keep the events. It’s the extreme case of reducing data near the source: keep the answer, drop the rows.
An Embedded Workflow Engine With Five Primitives: Trigger, Condition, Action, Wait and Retry
A user signs up on Monday. If they haven’t activated by Wednesday, you send a nudge email, and if the email provider times out, you try a few more times before flagging the account. That’s the whole spec. Most teams build it as a cron job that scans for users in the wrong state, a nullable nudge_sent_at column and a retry loop written late on a Friday. It holds until the cron overlaps itself, or a deploy lands mid-loop, or the provider times out after actually sending the message and one customer gets three emails.
An Evidence Graph Built From Extracted Claims Keeps Contradictions Attached to Their Sources
Feed forty articles about a rumored plant restart to a summarizer and you get one smooth paragraph: several outlets report that production will resume in March, though the ministry has pushed back. It reads well. It also throws away what you’d need to act on it. You can’t tell which outlets reported it, whether each had its own source or all copied one wire story, or what the ministry actually said, a denial or a polite refusal to comment.
Configuration Drift Lives Between Files, So Lint Env Files, YAML, TOML and CI Together
.env.example says DATABASE_URL=postgres://localhost:5432/app. The compose file the team uses for local work mounts a volume and sets DATABASE_URL=sqlite:///data/app.db. The CI workflow exports DB_URL, a name the code stopped reading two refactors ago. Production has a secret called DATABASE_URL that nobody has looked at since the last rotation. Each of those files is valid, and every linter you could point at them passes them. The app still behaves differently on three machines, and the first person to find out is whoever is on call.
Context Window Management as a Database: Ask for 12,000 Tokens, Get the Most Useful Ones
An agent is ninety tool calls into a refactor. Step 12 was a grep that returned 52 KB, and all of it is still in the prompt. The user’s instruction from step 1, “never touch db/migrations”, fell out of the window ten steps ago, because the framework drops the oldest messages first. The agent edits a migration. Nothing malfunctioned; the truncation rule did exactly what it was written to do.
Cron for AI Agents: Schedules, Timeouts, Overlap Rules and Cost Ceilings in One Config File
A nightly agent job triages new issues. One night an issue arrives with a 400 KB log attached. The agent reads it, a tool call fails, the agent retries, and the loop runs until the month’s token budget is gone. Meanwhile cron, which has no idea what a token is, starts the next run on schedule. Now two copies are fighting over the same issue tracker.
Cron was built for jobs that cost nothing, finish in seconds and do the same thing every time. Agent jobs cost money per token, run for an unpredictable time and can loop. The useful ones are still boring and periodic: summarize what changed in a repo since yesterday, triage new issues each morning, check a competitor’s pricing page weekly. What they need is a scheduler that stays as small as cron and adds four things: a cost ceiling, a timeout that actually kills, an overlap rule, and state carried between runs. Anything past that is an orchestration platform, a different and much larger product.
Database-to-Database Sync for Local-First Apps Lives or Dies on Conflict Rules
Take a field technician who edits a work order on a tablet in a basement with no signal. Upstairs, the office edits the same order from a desk. Both hit save at 10:42 by their own clocks, and one of those clocks is ninety seconds off. When the tablet finds a signal, the sync layer has to produce one order that both people can live with. That decision is the product. Offline-first apps are harder than they look, and a large share of the difficulty sits in this one step.
Debug From a Timeline of State Changes Instead of Ten Thousand Log Lines
A ticket comes in: order 8812 shows as cancelled, but the customer’s card was charged. You open log search. The checkout service wrote a couple of hundred lines for that request; the payment webhook handler wrote a few dozen; the job that expires unpaid orders wrote a line for every order it looked at that minute. The answer is in there somewhere. Forty minutes later you find it. The expiry job cancelled the order thirty seconds after checkout began, and the payment provider’s webhook landed half a second after that.
DuckDB Already Queries Every File in a Folder, So Build the Part That Infers the Joins
Someone hands you a zip: customers.csv from the CRM, a JSON dump from the billing API, and a SQLite file from an internal app nobody has touched in two years. DuckDB will make all three queryable in a few lines of SQL, with no import step (SELECT * FROM 'customers.csv' works as written). Then you spend the afternoon working out which column joins to which. The CRM says customer_id and stores 00123. The billing API says customerId and stores 123. The SQLite table has a column called customer holding the text 123. Same customer, three spellings.
Embedded Vector Search Is Crowded: What a One-File Vector Database Needs to Stand Out
You have 40,000 support articles and an app that should offer “similar articles”. Embedded at 768 dimensions in float32, that’s 40,000 × 768 × 4 bytes, about 123 MB of vectors. The standard advice is to stand up a vector database: a service, a port, credentials, a client library and a backup story for 123 MB. A file would do.
Below a few million vectors the server is the overhead, which is why the embedded field filled up fast. So the useful question is what a newcomer brings that the free libraries don’t. My answer is one sharp edge you can state in a sentence, plus measurements the others leave you to make yourself.
Facts Should Expire: A Database That Tells an AI Agent When Its Knowledge Has Gone Stale
An agent keeps a memory table, and two rows matter for today’s task: a supplier’s headquarters city, written eleven months ago, and a freight quote, written three hours ago. A SELECT returns both in the same shape. The headquarters is probably still right. The quote may already be wrong. Nothing in either row says so, and the agent repeats both in the same flat tone.
A row has no idea how old its truth is. It might carry a created_at, and a careful person can compute an age from it, but the decay rate belongs to the kind of fact, not to the table. A three-week-old headquarters address is fine. A three-week-old shipping rate is fiction. Server health goes bad in half a minute. No timestamp column holds that knowledge, and nothing forces a query to read it anyway.
Give Every Database Query a Budget and Fail the Build When It Scans Too Much
A developer changes a login lookup to WHERE lower(email) = lower($1) so that sign-in stops being case-sensitive. Every test passes. The btree index on email can’t serve a query that wraps the column in a function, so Postgres reads the whole users table, and on a test database with forty rows that’s the cheapest plan anyway. The change ships. A week later someone notices login latency tracking the signup count. The fix is a one-line expression index that would have taken a minute if anything had objected during review.
Group Errors by Behavioral Signature Instead of Stack Trace Text
Add six lines to the top of a file and ship it. Every stack trace through that file now carries different line numbers, so a tracker that hashes the trace files a bug it has seen a hundred times as a brand-new issue. Meanwhile an older issue titled TimeoutError has collected comments from people who each fixed a different timeout and wondered why it never closed.
Grouping fails in both directions at once. One bug turns into hundreds of issues because the message embeds an order ID, the line numbers moved, or a framework upgrade changed the frames in the middle. Two bugs turn into one because every timeout in the codebase raises the same exception type from the same retry helper.
Incremental View Maintenance Small Enough to Embed Turns Dashboard Queries Into Lookups
A dashboard tile shows today’s US traffic, so every page load runs SELECT count(*) FROM events WHERE country = 'US' over a table that grew by a few hundred thousand rows since yesterday. SQLite answers correctly. It also walks every matching index entry to do it, because its b-trees don’t keep counts, and it does the same walk for the next viewer and the one after. Between two page loads the answer moves by a handful of rows, yet each load pays for all of them.
Kafka for One Machine: An Embedded Event Log With Offsets, Compaction and No Broker
A desktop app wants an undo history that survives a crash. An edge gateway buffers sensor readings while its uplink is down and has to replay them, in order, once the link is back. An AI agent writes down every action it takes so a bad run can be replayed later. All three are the same data structure: an append-only sequence of records, plus readers that remember where they stopped. Kafka is that structure, and also a cluster, partitions and the people who tune them.
Mapping Every Dependency to the Features That Break If It Disappears Tomorrow
Run npm ls --all on a mid-sized web app and you get a tree that scrolls for pages. It’s accurate and complete, and on the morning a maintainer deletes a package it tells you almost nothing. You can see that tiny-util sits three levels down. You can’t see whether losing it breaks the checkout page or a script nobody has run since spring.
A dependency tree answers “what do I depend on”. The question that matters is “what breaks, and for whom, if this package vanishes or turns hostile”. They need different data. The tree comes from a lockfile. The answer comes from your own source: which functions call the package, which routes, commands and jobs those functions serve, and how much code would change to replace it. A tool that produced that for every package would turn dependency management from an inventory into something a product owner can read.
Most Log Volume Is a Few Templates Repeated, So Reduce Logs Locally Before Shipping Them
Take a day of production logs from one service, mask the numbers, IDs and quoted strings in every line, and sort the resulting patterns by how often they occur. The top of that list is short and dull: health checks, cache hits, a retry notice from a client library nobody owns. Each of those lines was serialized, shipped, parsed and indexed millions of times to say the same thing, and you paid for every copy. Run it on your own logs. The claim is easy to check, so don’t take it on faith.
One Binary, One File, No Daemon: The SQLite and nginx Pattern as a Product Strategy
Compare two quickstarts. The first says to install Docker, bring up a compose file with a database, a cache and a worker, create an account, and paste a token into an env file. The second says to download one file and run it. Both tools might solve the same problem. Only one of them survives an evaluation that lasts ten minutes.
The second quickstart is a product decision, made in the architecture. SQLite and nginx made it decades ago, and Caddy, DuckDB, ripgrep, jq and Hugo have made it again in their own corners. Small surface area, few dependencies and one artifact to copy work as an adoption strategy. For a one-person or AI-assisted project they also work as the only scope control that holds up. That’s the claim, and the rest of this post tries to break it.
Redis Semantics Without Redis: An Embedded State Engine for Counters, Leases and TTLs
A command-line tool should run only one copy of its nightly export at a time. A small web app wants to cap each user at 60 requests a minute. A desktop app needs to remember when it last synced. All three are state: an integer, a timestamp, an owner. All three usually get solved by adding Redis, because Redis has the verbs for them: INCR, SET NX PX, EXPIRE. The app needed the verbs and got a server along with them, a port and a persistence config included.
Spreadsheet Recalculation Inside a Database: Derived Values That Track Their Own Dependencies
A line item’s quantity goes from 4 to 5. The order subtotal should change, and so should the tax, the order total, the customer’s lifetime value and the regional revenue figure on a dashboard. Which of those actually update depends on which code path made the edit, because each one gets recomputed somewhere different: the checkout handler, the invoice renderer, a nightly job, a SQL view somebody wrote for finance. Every copy of the formula is another chance for two screens to disagree.
SQL and Key-Value Over the Same Bytes: An Old Database Idea Worth Making Small
A sensor node writes a reading every few seconds. The firmware wants one thing from storage: put a record by key, get it back by key, as fast as the flash allows. The dashboard on the gateway wants something else, like average temperature by sensor by hour, joined to a table of sites. The usual answer is two databases with a translation job between them, a key-value store on the device and SQLite or Postgres upstream. Then comes a year of debugging the translation: a unit that changed, a row that arrived twice, a field the device encodes one way and the gateway reads another.
Store Changes Instead of Current Rows: An Embedded Database Built Around History
A support ticket asks two plain questions: which plan was customer 4411 on when the August invoice went out, and who moved them off it? The customers table can’t answer either. It holds today’s row. The audit table someone added two years ago holds most of the story, apart from the week a data fix ran a bulk UPDATE in raw SQL and skipped the ORM callbacks that write the audit rows.
Verified Absence Is Evidence: A Database for Recording That You Checked and Nothing Was There
An auditor asks you to show that customer 1187 was screened against a sanctions list on March 3. The screening job writes a row when it finds a match, and customer 1187 has no rows. That fits two histories: the job ran and found nothing, or the job never ran. The database can’t tell them apart, so you hold no evidence either way.
This is the closed-world assumption doing what SQL has always done: a row that doesn’t exist is false. RDF and OWL went the other way with an open-world assumption, where a missing statement means unknown. Neither has a place for the sentence you actually want to store: I looked here, in this way, at this time, and found nothing.
Screening App Ideas Before You Write Code: Four Questions, Starting With Who Pays
App ideas cost nothing. Every one of them looks shippable in a weekend, and then the weekend turns into three months of auth flows, sync bugs, review notes and a paywall that took longer than the app.
So before Xcode or Android Studio opens, put the idea through four questions and write the answers down. Who pays? What problem does it solve? Can you say what it does in one sentence? Why would anyone switch, and what stops Apple from copying it? Ask them in that order. Money goes first because it kills weak ideas fastest, while a kill still costs an evening.
Nine Apps Worth Coding, and the Hard Part Buried in Each One
Every app idea has a boring 90% and a nasty 10%. The boring part is CRUD, auth, a settings page, Stripe. The nasty part is the one subsystem that decides whether the thing works at all, and it’s usually not the part that looks hard in the pitch. Below are nine builds worth attempting, with the nasty 10% named up front so you can decide whether you want to spend your weekends there.
The Side Project App Is Not Dead. The Side Project App Business Is.
The romantic narrative of the solo developer who builds an app in nights and weekends, publishes it to the App Store, and generates meaningful income from passive sales is not entirely false. It is sufficiently true in enough specific cases to sustain the mythology and sufficiently rare to make the mythology dangerous as a planning assumption.
The structural conditions that made the App Store a viable path to meaningful passive income for individual developers in 2010 to 2015 have changed. The App Store had fewer apps. Discovery was less competitive. Users were more willing to pay upfront for apps they had not tried. The apps that met a basic need in a well-executed way could find their audience without significant marketing investment.