π This is a cross-post. The original lives on my blog: What we learned from building Astroniq.
On May 2nd I made the first commit to an empty repo. Two months later it held 832 commits and roughly 284,000 lines of TypeScript: five frontend apps, four backend services, fifteen shared packages β all written, deployed, and maintained by one person.
The product is Astroniq, an AI-powered Vedic astrology platform. Not a horoscope widget with a chat skin β a real computational engine:
- Divisional charts down to D150 (Nadi Amsha), computed from raw ephemeris math.
- An agentic AI chat that doesn't hallucinate your chart β it calls the engine and reasons over real numbers (more on this below).
- Birth-time refinement that reverse-engineers an unknown birth time from the timeline of your life events, scoring candidates against dasha/transit patterns.
I'm not listing this to flex. It's the setup for the actual lesson:
If one person can ship all of that in two months, building is no longer the hard part of a startup. Something else is.
Let me show you the engineering first, because that's the fun part β then the thing that blindsided me.
What breaks when you're the entire engineering team
Solo dev with AI tooling is a new sport. The bottleneck isn't typing code β it's judgment and catching what breaks. And things break in ways no tutorial warns you about.
1. Your deploy pipeline will try to kill your product
My first deploy was the one-liner every tutorial teaches:
docker compose up -d --build
Looks harmless. It has a latent outage baked in. up --build recreates the app containers, then gates their startup on the migration container via depends_on: { migrate: { condition: service_completed_successfully } }. So the sequence is: tear down the running app β run migrations β start new app. If a migration exits non-zero, the old containers are already gone and the new ones are stuck in Created. Full outage β caused by the safety check itself.
The fix was to invert the order so a bad deploy is invisible instead of fatal:
set -e
# 1. Build first. A build failure changes zero running containers.
docker compose build
# 2. Run migrations as an explicit one-shot gate. A non-zero exit aborts
# the script here (set -e) β the LIVE containers keep serving the old image.
docker compose run --rm migrate
# 3. Only now roll the app onto the new images.
docker compose up -d --no-deps api-service-1 api-service-2 api-service-3
Same tools. Completely different failure semantics. A broken migration now stays invisible to users instead of taking the site down.
2. Disks fill up at 10:49 PM on a Friday
One evening Redis started failing its background saves:
redis | Write error while saving DB to disk: No space left on device
The host had 787 GB free. The container's VM did not. docker system df told the story:
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 9 6 19.12GB 3.89GB (20%)
Build Cache 66 0 8.09GB 8.09GB (100%)
Not logs. Not data. 8 GB of build cache + 3.9 GB of orphaned images, one dangling image left behind by every --build deploy, quietly accumulating on a 47 GB disk until a deploy tipped it to 100% and starved Redis mid-BGSAVE.
The permanent fix was one line added after a successful rollout β each deploy rebuilds the app image under the same tag, orphaning the previous one:
# After the roll: reclaim the now-dangling previous image. `-f` = dangling only,
# so images backing running containers are untouched.
docker image prune -f
Nobody puts "your CI leaves a 4 GB corpse on the server every deploy" in the getting-started docs.
3. The internet attacks you on day one β and Redis is the sharp edge
Within days of exposing Postgres on a VPS, my logs filled with credential-stuffing β dictionaries of usernames, around the clock, from scanners that hunt fresh servers.
Redis is scarier than Postgres here. Default Redis has no auth, and once you're connected it's a one-shot RCE: CONFIG SET dir /root/.ssh + SAVE writes an attacker-controlled authorized_keys. So Redis never gets published to the host:
redis:
image: redis:7
# No `ports:` β reachable ONLY on the internal docker network.
# requirepass is belt-and-braces so a popped app container still can't
# read/write the cache. `:?` makes compose refuse to boot if it's unset.
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD:?must be set}"]
expose: ["6379"]
Plus ufw deny 6379/tcp re-asserted on every deploy, so one careless compose edit can't silently re-open the door.
4. APIs lie, or at least their docs do
This one cost me an afternoon. An image-generation API's docs said the bytes come back at output_image.data. My extraction kept throwing "no image data" even though the request clearly succeeded and billed me for the tokens.
The live response didn't have an output_image object at all. The bytes were nested inside a steps[] array, on a field called signature, one level deeper than documented:
// What the docs promised:
const data = interaction.output_image?.data; // β always undefined
// What the endpoint actually returns: steps[] wrapping a content[] with the
// media part. So walk the tree instead of trusting a fixed shape.
const findPart = (parts, type) => {
for (const p of parts ?? []) {
if (p.type === type) return p;
const nested = findPart(p.content, type); // depth-first
if (nested) return nested;
}
};
const part = findPart(interaction.steps, "image");
const bytes = Buffer.from(part.signature, "base64");
My git history has five consecutive commits with the identical message. That's what debugging a third-party API in production looks like when you're the whole team.
The agentic chat: don't let the model invent your Moon sign
The one piece I'm proudest of: the AI chat never guesses chart data. LLMs will happily hallucinate "your Saturn is in the 7th house" with total confidence. So the model doesn't get to compute anything astrological β it gets tools, and the tools run the real ephemeris:
const tools = [{
name: "get_planet_positions",
description: "Sidereal planet longitudes + house placements for a chart.",
input_schema: { /* birth data */ },
}];
// Flow: user asks a question
// β model calls get_planet_positions(birthData)
// β the deterministic engine computes real positions
// β model reasons over ACTUAL numbers, then writes the answer.
The interpretation is generated; the facts are computed. That boundary is the whole product.
Here's the inspirational bit, and I mean it: every one of these was solvable in hours or days. Hostile internet, fragile deploys, full disks, lying docs β each has a deterministic answer. You find it, you fix it, it stays fixed. If you're a builder sitting on an idea: the technical mountain is climbable, solo, faster than you think.
Which is exactly why the next thing blindsided me.
The realization: development is easier than distribution
Around week six I noticed something uncomfortable. Shipping a new divisional-chart lens β real ephemeris math, a whole analysis layer β took about a week. Getting anyone to know it existed was eating more than that. Every week. Forever.
The asymmetry is structural, and naming it changed how I think about shipping:
Development compounds. Distribution β the way solo devs do it β doesn't.
When I write code I stand on forty years of compounding tooling. The compiler catches type errors. Tests catch regressions. CI catches broken builds. Every safety net I build stays built β my 200th feature ships faster than my 20th because the infrastructure remembers.
When I did distribution, I was a caveman with a stick. Every morning, from zero: what do I post? Is anything trending? Which article is decaying in search? Did that reel do well β make a part two? Nothing compounded. The 200th post took exactly as long as the 20th.
And distribution is engineering β I just wasn't treating it that way. Case in point, a bug I shipped in my own sitemap:
// The sitemap fetched every published slug to emit <url> entries:
await fetchBlogList({ lang, limit: 200 });
// β¦but the public list API capped `limit` at 50:
limit: z.coerce.number().int().min(1).max(50) // 200 β 400 Bad Request
// The fetch swallowed the error and returned []. Every blog post silently
// vanished from the sitemap. Google literally could not discover them.
A distribution failure that was really an off-by-a-Zod-constraint engineering bug. Fixed by paginating within the cap. It made the point for me: your distribution hours are your most expensive hours, because they're the only ones without power tools. So I gave them power tools.
Treating growth like infrastructure
The question I needed answered every morning was embarrassingly simple: "What should I do today to grow this?" Not a content calendar. Not 40 AI posts to wade through. One ranked list of concrete actions, each with a reason and a one-click resolution.
The abstraction I landed on is the same shape I'd use for any event pipeline β a detected signal β recommended action β one-click resolution, ranked by impact-per-minute:
Trend signals βββ
(astro cal, βββΆ Detector plugins ββΆ Opportunity { β
reddit, β (one per signal, β ranked by
search) β growth lever) action, β impact Γ·
Engine insights β resolution, β effort_minutes
provenance } β
β
βΌ
Morning brief β human approves
Three engineering decisions that mattered:
- Timely Γ True. Generic AI content is spam and readers smell it. But the product sits on a deterministic trend calendar β planetary events are computable months ahead from the same ephemeris engine. Cross a timely hook with a grounded, engine-computed insight and the content is both relevant and true. Your product probably has an equivalent: the data only you have is the content only you can make.
- Everything carries provenance. Every draft records exactly what it was generated from β which engine insight, which trend signal. If AI drafts in your name, receipts are non-negotiable.
- A human approves everything. The system detects, ranks, drafts, and routes; I decide. It removes the searching and blank-page-staring, not the judgment. Everything upstream of judgment is a pipeline.
Daily growth work went from hours of scattered anxiety to a ~15-minute morning ritual β and those minutes now compound the way my dev hours always have.
Three things I'd tell you on day one
- The technical mountain is climbable, alone. A quarter-million lines, an engine, an AI layer, mobile apps, billing, infra β one person, two months. Whatever you're afraid you can't build: you probably can. Start.
- Notice which of your hours compound. Dev hours compound because the tooling remembers. If your distribution hours evaporate at midnight, that's not a discipline problem β it's missing infrastructure. Build it or find it; don't just try harder.
- Ship the product, then engineer the megaphone. Distribution deserves the same rigor as your deploy pipeline: detection, prioritization, provenance, repeatability.
And keep an eye on your disk space. Redis will pick a Friday night. It always picks a Friday night.
Full disclosure: I built the growth system above for my own product, so I'm biased about the approach. But the engineering lessons stand on their own β and I'd genuinely love to hear how other solo devs are attacking the distribution half. What's compounding for you, and what still isn't?
Originally published on the Astroniq blog.
Top comments (0)