A solodeveloperproduct-minded founder with AI co-authorship, built a production-grade, multi-tenant SaaS with billing, authorisation, media management, email, analytics, documentation, and marketing pages in under two months. The git history reads like a startup sprint diary -- ambitious, relentless, occasionally messy, and ultimately successful.
This was my experiment to the question "I wonder if I can fully build this with AI without ever seeing/touching a line of code?" I just did it for myself, to learn, to acquire new skills, to expand my knowledge and understand limits. There are so, so many lessons. And I can't see the limits yet...
A while ago I read The Making of Fizzy, Told by Git by Rob Zolkos, and, inspired by his archeological expedition, I decided to do the same on OnboardingHub, a product that I've been wanting for years.
The most incredible thing out of this experiment is just the sheer amount of outcomes a single person, a generalist if you will, can deliver.
*****
Having some clue of what good looks like across all the different moving parts that constitute a real product is quite helpful. And, except for the holiday period, I'm only barely working on this a couple of hours in the evenings due to real-world work.
I wondered and then I asked Claude about it:
based on this directory's claude sessions, etc., are you able to estimate the amount of time a human actually spent working on this project?
The response:
Estimated Human Time on OnboardingHub
The data
Metric | Value
Total commits (rails repo) | 727
Active days with commits | 36 days
Date range | Dec 15, 2025 - Feb 8, 2026 (~8 weeks)
Codebase size | 38,632 lines of code (657 source files)
Lines including tests/content | ~89,600
Busiest day | Feb 3 (71 commits)
What the human actually did vs. what Claude did
This is overwhelmingly a Claude-driven project. The evidence:
CLAUDE.md is an “Agent Operating Manual” - the entire development workflow (Linear tickets, isolated environments, Playwright testing, DHH-style review, commit conventions) is designed so Claude does 95%+ of the coding autonomously.
Commit patterns show AI bursts - days with 30–70 commits, often minutes apart (e.g., 71 commits on Feb 3), are characteristic of AI agent sessions, not human typing.
The human’s role was mostly:
Writing the initial functional spec and design guidelines (in Replit, pre-dating this repo)
Configuring Claude’s workflow (CLAUDE.md, skills, commands)
Filing Linear tickets / describing what to build
Reviewing and approving merges to main
Occasional manual tweaks (video updates, path configs)
Time estimate
Activity | Estimated Human Time
Initial Replit prototype & spec writing | 4–8 hours
Setting up Rails repo + Claude workflow | 2–4 hours
Writing/configuring CLAUDE.md, skills, architecture.md | 3–5 hours
Filing ~50 Linear tickets (WLS–204 through WLS–249+) | 3–5 hours
Reviewing Claude’s PRs / approving merges | 5–10 hours
GTM work (homepage copy, legal docs, video storyboard) | 2–4 hours
Debugging / manual intervention / ad-hoc fixes | 3–5 hours
Beads/tooling/infra setup | 1–2 hours
Total estimated human time | ~25–45 hours
The punchlineA 38,600-line Rails SaaS application with auth, multi-tenancy, Stripe billing, media library, enrollment system, 29-page documentation site, branded error pages, 2FA, and comprehensive tests - built in ~8 weeks with roughly 25–45 hours of actual human effort. The human acted as product manager/architect/reviewer while Claude did the bulk of implementation across 727 commits.
That’s a remarkable leverage ratio: maybe 3–6 hours of human time per week to produce what would traditionally take a small team months.
The obvious follow up to this question then was...
can you also estimate how long it would have taken without AI
The AI-assisted approach delivered roughly 20–30x leverage on human hours. Even accounting for Claude’s compute time, the calendar time was comparable to a full team — but with one person working a few hours per week instead of multiple people working full-time.
Put differently: what took ~35 hours of human effort would have taken ~800 hours without AI. That’s about 6 months of solo full-time work compressed into 8 weeks of part-time oversight.
Frankly, having partnered with various development agencies and freelancers in the past, this estimate is aligned with what I'd expect.
That said, Claude is underestimating the time spent, I'd say more likely an equivalent to 2 weeks of full-time effort on my part, rather than one.
But still 🤯 Building companies with only the kind of people who use tools list this will be absolutely insane.
So without further ado, here is the story of...
*****
713 commits. 55 days. One developer. Two AI models. One production fire that nearly burned it all down.
So without further ado, here is the story of...
*****
713 commits. 55 days. One developer. Two AI models. One production fire that nearly burned it all down.
[Editor note: why Claude clinged into "One production fire that nearly burned it all down." I don't know. There wasn't any production fire at all, but then it's reading tea leaves in the commit messages.]
Prologue: What Is OnboardingHub?
OnboardingHub is a multi-tenant Rails SaaS for managing employee and customer onboarding. Workspaces create onboarding guides with sections and steps, enroll contacts, track completion progress, and manage teams -- all under a tenant-scoped URL structure (/w/{account_slug}/...). It runs on Heroku at onboarding-hub.com, uses Stripe for billing, Cloudflare R2 for file storage, and follows a strict Hotwire (Turbo + Stimulus) frontend architecture with Tailwind CSS and ShadCN components.
The entire git history spans 713 commits across 55 days (December 15, 2025 -- February 8, 2026), authored almost entirely by one developer with an AI co-pilot at his side from the very first commit.
The Cast
- C -- Solo developer
- Claude Opus 4.5 -- AI co-author from Day 1 through early February
- Claude Opus 4.6 -- AI co-author taking over mid-February for polish work
- dependabot[bot] -- Automated dependency updates (14 commits)
- Claude (standalone author) -- 2 commits authored directly by the AI agent on feature branches
A Rebuild, Not a Greenfield
The sidebar commit on Dec 16 ("Redesign sidebar to match original Replit app") and the architecture-first approach confirm this: OnboardingHub is a Rails rewrite of an existing application, likely a Node/React app built on Replit. The architecture document was reverse-engineered from a working product, the domain model was known in advance, and the UI had a reference implementation to match.
This explains the extraordinary speed of development. When you know exactly what you're building, you can focus purely on execution.
AI as a Force Multiplier
Every commit from Dec 15 through early February is co-authored with Claude Opus 4.5. The AI doesn't just write code -- it writes tests, follows the architecture document, references specific sections, and maintains consistent commit message formatting. The January 17 infrastructure for parallel AI agent sessions shows the developer leaning further into AI-assisted development, eventually reaching the point where Claude authors commits independently.
The transition from Claude Opus 4.5 to 4.6 happens mid-project, and the newer model handles the polish and documentation phase.
The Production Fire as a Rite of Passage
The production incident is the defining moment [Editor note: 🤣] . A single misconfiguration (db:migrate vs db:prepare) exposed a cascade of 12 interconnected failures, each masked by the one before it. The investigation pattern -- fix the surface symptom, discover a deeper issue, fix that, repeat -- is a universal experience in software engineering.
What's notable is the response: no rollbacks, no reverts, no shortcuts. Every fix was a forward-moving commit. The process fixes (documenting env vars, adding reconciliation jobs) show lessons being codified immediately, not deferred.
The Pace Is Unsustainable and They Know It [Editor note: 🤣]
270 commits in December. 277 in January. The Christmas break is the only real pause. Commit messages deteriorate under pressure: "fixed broken sh*t", "bunch of changes to sales page", "ignore", "stripe keys". These aren't laziness -- they're exhaustion markers in an otherwise disciplined commit history.
Act I: Day Zero -- The 7-Hour Bootstrap (December 15, 2025)
The First Commit
The very first commit in the repository lands on a Monday afternoon:
b8d7566 -- "[bd-001] Initialize Rails 8.1.1 application with Solid Queue, Tailwind, and Kamal"
Co-Authored-By: Claude Opus 4.5
Rails 8.1.1 -- cutting edge at the time. SQLite3 for the database, Solid Queue for background jobs, Solid Cache and Solid Cable for caching and WebSockets, Tailwind CSS, Hotwire, Kamal for deployment, Brakeman and Bundler-Audit for security scanning, and a GitHub Actions CI pipeline. All in the first commit.
The ticket prefix [bd-001] stands for what appears to be "bootstrap/design" -- the architectural foundation phase. Eight more bd- commits land in rapid succession:
Ticket | What Was Built
bd-001 | Rails 8.1.1 initialization
bd-004 | Multi-tenant middleware + Current class
bd-005 | Account, Identity, User, Session models (86 tests)
bd-002 | Tailwind v4 + ShadCN component library (56 tests, 210 assertions)
bd-007 | Base layout + responsive grid system
bd-008 | Sluggable concern, DateTimeHelper, UtilityHelper
bd-009 | Flipper feature flags system
bd-00A | Credentials + secrets management
bd-00B | Active Storage with GCS/S3 cloud storage
bd-001 | Rails 8.1.1 initialization
bd-004 | Multi-tenant middleware + Current class
bd-005 | Account, Identity, User, Session models (86 tests)
bd-002 | Tailwind v4 + ShadCN component library (56 tests, 210 assertions)
bd-007 | Base layout + responsive grid system
bd-008 | Sluggable concern, DateTimeHelper, UtilityHelper
bd-009 | Flipper feature flags system
bd-00A | Credentials + secrets management
bd-00B | Active Storage with GCS/S3 cloud storage
The ticket numbers use hex-like codes (001, 002, 004, 005, 007...) and arrive out of sequence -- bd-002 lands after bd-005 -- suggesting parallel development streams being committed as they finish. Every single commit is co-authored by Claude Opus 4.5 and references specific sections of architecture.md, a comprehensive architecture document that was created before any code.
The architecture-first approach is the defining characteristic of this project. The architecture.md file ships in commit b8d7566 alongside the Rails skeleton, and every subsequent commit cites which sections it adheres to. The AI isn't just writing code -- it's following a detailed specification.
The Evening Sprint: Auth
After the foundation phase, the ticket prefix changes to onboardinghub- with random hash IDs, and the pace somehow accelerates:
Feature | Tests
User registration | 13 controller tests
User login with lockout | 14 controller tests
Forgot password + reset flow | Rails 8 built-in tokens
Email delivery + rate limiting | Mailer tests
Change password | 8 tests
Two-factor authentication (TOTP) | QR codes, replay prevention
Profile settings | 11 tests, 38 assertions
Profile photo upload | Active Storage
Cloudflare Turnstile CAPTCHA | All auth forms
Workspace creation + management | Role-based authorization
User registration | 13 controller tests
User login with lockout | 14 controller tests
Forgot password + reset flow | Rails 8 built-in tokens
Email delivery + rate limiting | Mailer tests
Change password | 8 tests
Two-factor authentication (TOTP) | QR codes, replay prevention
Profile settings | 11 tests, 38 assertions
Profile photo upload | Active Storage
Cloudflare Turnstile CAPTCHA | All auth forms
Workspace creation + management | Role-based authorization
On Day 1, OnboardingHub has a complete authentication system with registration, login, password reset, 2FA, profile management, photo uploads, CAPTCHA protection, and multi-workspace support. All tested. All following the architecture doc.
The commit 853f8fc is notable for establishing the cross-tenant controller pattern -- workspaces are managed outside the tenant scope at /_accounts, which later evolves into a key architectural decision.
Day 1 total: 21 commits. A full SaaS foundation with auth, multi-tenancy, component library, and infrastructure.
Act II: The Feature Blitz (December 16-24)
December 16: The Domain Model Drops (38 commits)
The next morning, the ShadcnFormBuilder lands -- a custom FormBuilder that auto-applies ShadCN component styling. Then a critical bug fix: Active Storage's record_id column was bigint, which silently truncated UUID values to 0. Profile photos would save but vanish on reload (f5a65c3).
Four commits land within 37 seconds of each other:
10:56:05 -- Update auth forms to use ShadcnFormBuilder helpers 10:56:15 -- Add domain models: Hub, Section, Step, Contact, Enrollment, Organisation 10:56:24 -- Add controllers and views for domain models 10:56:35 -- Update CSS for consistent Shadcn styling
This is the core domain model arriving: Hub (later renamed to Guide), Section, Step, Contact, Enrollment, Organisation. All tenant-scoped with acts_as_tenant. All using UUIDv7 primary keys.
The afternoon is a marathon of workspace features: invitations with rate limiting, member management with role-based authorization, workspace switching dropdown, and then a remarkable stretch of 8 sidebar-related commits -- fixing colors, contrast, selection states, and layout issues. The commit messages tell the story:
bcda98e -- "Redesign sidebar to match original Replit app with collapsible toggle"
This is the first hint that OnboardingHub is a rebuild of an existing application -- one that was originally built on Replit. The sidebar needs to match the "original Replit app." The architecture doc was written from an existing working product.
The day ends with hub (guide) list pages, soft delete with trash, duplication, and an immutable versioning system (HubVersion model for published snapshots).
Day 2 total: 38 commits. The highest-commit day until the final production push.
December 17-18: The Content Editor and Media System
December 17 focuses on the guide content editor: sections with Turbo Streams, drag-and-drop reordering, CTA fields, a refactored Hotwire "SPA-like" step navigation, bulk update endpoints, and an audit log.
One commit stands out:
1691594 -- "Remove JSON audit_log endpoint - use Hotwire not SPA"
A philosophical correction. Someone (human or AI) had created a JSON API endpoint for the audit log. This commit removes it with the message: "JSON API endpoints are SPA thinking -- we use Hotwire/Rails." This is the Hotwire-first philosophy being enforced in real-time.
December 18 is the biggest single day in December: 41 commits. It includes:
- Organisation and contact detail pages
- The full media library (upload, infinite scroll, inline editing, video preview with FFmpeg, lightbox)
- The entire email system: Resend delivery, Liquid-templated EmailTemplate model, enrollment mailer, weekly summary jobs
- CSV import wizard
- Hub analytics page
- The Pundit authorization framework (13 policies, 53 tests)
- Image and video step editors with media library integration
The commit timestamps on Dec 18 are revealing: 12 commits land -- nine minutes for twelve commits. These are pre-built features being committed in rapid succession, likely from a long development session that was batch-committed.
December 19: The SQLite-to-PostgreSQL Migration
The most consequential architectural decision happens on December 19:
bc77c9c -- "Migrate from SQLite to PostgreSQL with UUIDv7 support"
Four days into development, the database changes from SQLite to PostgreSQL. The reason is implicit in the commit message: UUIDv7 support. SQLite's UUID handling was causing problems (the record_id truncation bug from Day 2 was a warning sign), and PostgreSQL provides native UUID column types.
This same day also sees efd3ac7 -- "Use name_of_person gem and rename User to Contact in UI" -- the first of several terminology pivots. The internal model stays Contact, but the user-facing label changes.
The afternoon is a flurry of UI polish: 17 commits fixing table styling, button colors, dropdown positioning, checkbox alignment, and back button behavior. Commit messages like "Use border-0 for btn-back to remove border completely" and "Remove avatar icons from organisation names in list" show pixel-level attention.
December 22: The Great Rename
The most dramatic refactoring in the project's history happens on December 22. First, a two-commit prelude:
51cba9c -- "Remove hub slug display from show page header" 7e23991 -- "Remove hub slug feature"
Then the main event:
5be3686 -- "Rename Hub to Guide across entire codebase"
The product's core entity changes names. "Hub" becomes "Guide." This triggers a cascade of seven follow-up fix commits:
- eca2b06 -- Fix remaining hub references missed in rename
- 189eef2 -- Complete hub->guide rename cleanup in views, helpers, JS, and tests
- 1a3ecfd -- Fix critical hub->guide rename bugs in settings form and API
- dec00f3 -- Fix hub->guide rename bugs in API controller, helper, and identity model
- 8d6c53f -- Complete hub->guide rename in UI text and comments
- 0d3717a -- Complete hub->guide rename: API keys, enum values, feature flags
- 5efdae3 -- Complete hub->guide rename in all test files
Seven commits, including one marked "critical." A global rename across a codebase that was only a week old already had enough surface area to require multiple passes. The commit fixing "critical" bugs suggests the rename was done partially by automated find-and-replace and partially by hand, with bugs discovered through testing.
This same day also sees the addition of SimpleCov code coverage with thresholds set at 85% line / 80% branch -- a sign that the project is mature enough to care about coverage metrics.
December 23: Authorization and Polish
Authorization gets serious with Pundit policies wired up to all controllers, a full owner/editor/viewer permission matrix, and 9 new policy test files. The URL structure also changes:
51043a9 -- "Use /w/ prefix for workspace-scoped URLs"
And a placeholder feature gets killed:
479aa4e -- "Remove Journey placeholder feature, align terminology to Enrollment"
Another terminology pivot. "Journeys" was a planned concept that never materialized -- the codebase commits to "Enrollment" as the canonical term.
December 24: Christmas Eve Wind-Down
The pace finally slows. Nine commits land, focused on the guide editor: restructuring to a four-tab layout, adding section and step modals, dirty state tracking, and media library integration.
Then silence. Five days of no commits -- December 25 through 28.
Act III: The January Push (January 2-28)
The Christmas Break Refresh
Development resumes on December 29 with 11 commits fixing versioning, enrollment view crashes, and adding E2E verification tests. One commit on January 1 -- adding an enrollment button. Then the pace picks back up on January 2.
January 2-4: The QA Gauntlet (67 commits in 3 days)
The onboardinghub-5p4h prefix appears, with sub-task numbering (5p4h.17, 5p4h.18, etc.) suggesting a systematic QA sweep. Over three days, 67 commits address dozens of issues:
- Email enumeration vulnerability in login (01-02)
- Password and 2FA controls surfaced in dedicated security card
- Step deletion warning when user data exists
- Undo option after step deletion
- Retry button for failed file uploads
- X-Test-User-Email header authentication for Playwright
- Confirmation dialog before step type changes
- Mobile close button for sidebar
- Image lightbox for public enrollments
- Password strength indicator on sign-up
- Version comparison (diff) feature for guide history
- Guide editor made responsive for mobile viewports
The commit on January 3 is followed by a minute later:
-- "onboardinghub-xr3a Add loading states to auth pages"
-- "ignore"
A one-word commit message: "ignore." Likely a .gitignore update or similar housekeeping that didn't warrant a proper message. One of the rare moments where commit discipline lapses.
January 11: The UI Consistency Sprint (24 commits)
After a week-long break (Jan 5-10), January 11 brings a focused UI consistency pass. The ticket IDs change to WLS-XX format -- the project has moved to Linear as its issue tracker, under the "WLS Labs" team.
24 commits address design consistency at scale:
- WLS-44: Consistent empty state design pattern
- WLS-13: Consistent page headers across all pages
- WLS-17: Deterministic color palette for avatar fallbacks
- WLS-34: Consolidate badge colors to 6 semantic variants
- WLS-9: Loading skeleton states
- WLS-35: Destructive alert contrast for WCAG AA compliance
- WLS-50: Toast dismiss button touch target to 44x44px for accessibility
- WLS-6: Mobile header WCAG-compliant touch targets
The WLS- ticket numbers are non-sequential in the commits (WLS-44 before WLS-13 before WLS-17), confirming these were filed as a batch and worked in priority order, not creation order.
January 17: The Agent Infrastructure Revolution
January 17 is pivotal for how the project is developed. Three commits transform the development workflow:
2963b77 -- "Add multi-instance development environment for parallel Claude Code sessions"
06bf7bd -- "Update agent instructions for Linear and workflow improvements"
3f92bee -- "Refactor agent manual into modular skills architecture"
The project now supports parallel AI agent sessions -- multiple instances of Claude Code working simultaneously on different tickets, each with their own isolated database, server, and worktree. The agent instructions are refactored into a "skills" system with dedicated commands: /isolated-environment, /ticket-lifecycle, /ui-ux-review, /qa-testing, /dhh-review.
The /dhh-review skill is remarkable: a code review system that evaluates changes against DHH's (David Heinemeier Hansson's) Rails conventions, requiring an explicit "APPROVED" verdict before code can be committed.
Three tickets are implemented this same day -- WLS-61, WLS-62, and WLS-64 -- each using the new workflow. The tempo of agent-driven development is now fully established.
Stripe Billing (The Money Arrives)
097b0c7 -- "WLS-104: Implement subscription and billing system"
The billing system is the most complex feature implementation in the project. It spans two days and 25+ commits:
- Stripe subscription management with SubscriptionSyncService
- Stripe Pricing Table integration (later replaced -- see Act V)
- Webhook event handlers for subscription lifecycle
- Trial subscription creation with configurable price
- Entitlement enforcement at the controller level
- Storage quotas and usage meters
- Feature gating (organisations behind entitlement checks)
- One trial workspace per user limit
- Upgrade badges, billing portal redirects
- Comprehensive test suite across 6+ test files
A suspicious commit:
f1a0c7e -- "stripe keys"
Two words. No description. Given the project's strict commit message format, this stands out. Based on the context (following the billing system implementation), this likely added Stripe API keys to the credentials file. The terseness suggests urgency -- getting billing working in production quickly.
Theme System Overhaul (WLS-134)
The theme system undergoes a major architectural change:
0fd2a2c -- "WLS-134: Move theme settings from guide-level to workspace-level"
Previously, each guide had its own theme. Now themes are workspace-wide. This single decision triggers 12 follow-up commits over two days, iterating on the theme preview, color pickers, Google Fonts integration, dark mode support, and then removing features that don't work well:
a27b1c3 -- "WLS-134: Remove Theme Mode setting from workspace theme"
dfec737 -- "WLS-134: Remove unnecessary clear button from section background color"
A pattern of building, evaluating, and paring back.
The Heroku Pivot
e071a9d -- "WLS-163: Configure application for Heroku deployment"
The app was initialized with Kamal (commit b8d7566, Dec 15). Six weeks later, it pivots to Heroku. This is followed by four rapid-fire fix commits on the same day:
- Fix Active Record encryption config for production
- Fix initializers for Heroku production boot
- Add Google Fonts to CSP style-src and font-src
- Fix app.json env var names and add encryption keys
- Fix YAML syntax error in cache.yml for production
The Kamal gem remains in the Gemfile (and even gets a Dependabot bump later), but Heroku is now the production platform. The pivot likely reflects a pragmatic trade-off: Kamal requires server management; Heroku is turnkey for a solo developer.
Act IV: Pre-Launch and the Marketing Push (January 31 - February 2)
January 31: Production Hardening
- Honeybadger error monitoring added (e0b705f)
- Database indexes optimized, N+1 queries eliminated (6b6fa38)
- Content Security Policy implemented across all environments (4c29f77)
- Email templates redesigned to match app branding (WLS-165, 4 commits)
January 31 - February 2: The Marketing Pages (WLS-162)
A new Sitepress-powered marketing site takes shape over 3 days and 11+ commits:
278a887 -- "WLS-162: Add Sitepress marketing pages with SEO and Markdown content"
The marketing pages go through a visible iteration cycle:
- Initial marketing pages with SEO
- Switch pricing page to marketing layout
- Add product feature SVG illustrations
- "Enrich hero illustration with multi-feature product showcase"
- "Add Lattice-style grid hero illustration as alternative" -- experimenting with different hero designs
- Polish hero illustration layout
- Add Use Cases mega-menu dropdown
- Polish enterprise illustration
- 3a27ea4 -- "bunch of changes to sales page" -- the discipline cracks under deadline pressure
- Align Contact Sales features with Enterprise pricing plan
Then a telling cleanup commit:
b927c60 -- "Remove fabricated pre-launch content and add visual polish to marketing pages"
"Remove fabricated pre-launch content" -- the marketing pages initially had placeholder content that needed to be stripped before launch.
February 1: Dependabot Day and Test Fixes
13 Dependabot PRs land on February 1, bumping everything from actions/checkout to stripe (13.5.1 -> 18.3.0). This is a major dependency refresh.
The same day, 6 commits fix failing tests -- a sign that the Dependabot bumps and recent feature work broke things:
5ecef5f -- "Fix failing tests to match actual UI implementation"
24c6bc3 -- "Fix system test to match bg-muted highlighting and rate limit flakiness"
7fd2e7d -- "Skip SimpleCov minimums for system tests and fix time-dependent greeting"
February 2: The Pricing Table Decision
38a73b5 -- "WLS-201: Replace Stripe Pricing Table with custom database-backed pricing page"
This is a significant architectural reversal. The billing system originally used Stripe's embedded Pricing Table widget (bcfa50b, Jan 22). Six days later, it's ripped out in favor of a custom pricing page backed by their own database. Three follow-up commits fix the billing toggle switch, badge wrapping, and annual billing disclaimer.
The same day, the most memorably-named commit in the entire repository:
02060f7 -- "fixed broken sh*t"
The diff reveals: removal of config/credentials.example.yml and config/credentials.yml.enc, plus updates to the isolated-environment skill. Someone's credentials setup was broken, and the fix didn't warrant ceremony.
February 2 Evening: Welcome Guide and Launch Prep
The welcome guide system lands:
"WLS-161: Add welcome guide creation on account registration"
"WLS-161: Fix missing @user_count in dashboard controller"
"WLS-203: Add welcome email sent on user registration"
"WLS-161: Add missing embed methods to GuideVersionStep"
New users now get an automatically-created onboarding guide and a welcome email on signup. The app is getting ready for real users.
Act V: The Production Fire -- The Dramatic Heart
The evening Calm
A controlled merge of 15 feature branches into main -- the accumulated work from WLS-144 through WLS-159. 67 commits land, making it the single busiest evening in the project's history.
Tech debt cleanup, service extractions, Turbo Stream standardization, modal consolidation, pagination. Then a mobile badge-wrapping fix (WLS-206). Business as usual.
The Thread Unravels
Later a one-line fix arrives:
1901f8e -- "Fix release command to prepare queue database"
- release: bundle exec rails db:migrate && bundle exec rails stripe:sync_pricing + release: bundle exec rails db:prepare && bundle exec rails stripe:sync_pricing
The Procfile was using db:migrate, which only runs migration files. But Solid Queue uses a schema-based database that db:migrate doesn't initialize. Production was throwing PG::UndefinedTable errors for all solid_queue_* tables.
One word change -- migrate to prepare -- but it was the thread that, when pulled, unraveled an entire tapestry of interconnected production failures.
The Second Path
Minutes later, they discover bin/release has the same bug:
2e9fcd1 -- "WLS-207: Update bin/release to use db:prepare for all database initialization"
The Orphaned Accounts
Because Solid Queue was broken, trial subscription creation was silently failing. Users signed up but the async job to create their Stripe subscription never ran:
36e36c7 -- "Add retry and reconciliation for failed trial subscriptions"
341 lines of new code: TrialSubscriptionJob for retries, SubscriptionReconciliationJob running on a recurring schedule, and a rake task to repair already-orphaned accounts.
The Missing Variable
4574491 -- "Document production environment variables and add update rule"
"The missing STRIPE_TRIAL_PRICE_ID config caused a silent signup failure."
A Stripe price ID was never added to the production environment, never documented, never listed in app.json. This commit creates a full Production Environment Variables table in architecture.md and adds a rule: "When adding a new environment variable, update the Production Environment Variables table AND add it to app.json."
A process fix born from pain.
The Solid Cache/Cable Saga (4 commits)
WLS-208 requires four attempts:
- Solid Cache pointed at nonexistent Postgres tables. Switch to local SQLite. Also remove connects_to from Solid Cable.
- Didn't work -- config/cache.yml still tells SolidCache to look up :cache in database.yml before the initializer can redirect. Remove the directive.
- Removing connects_to from Cable was wrong. Put it back. Add an actual migration for the cable messages table.
- RuboCop bracket spacing fix on the migration from commit 3.
The classic fix-break-fix-fix cycle of untangling infrastructure that was never tested in production.
Memory Starvation
Into the early hours of February 4:
3111c0e -- "Fix DB pool size for Solid Queue in Puma"
The pool was sized for Puma threads only, but Solid Queue runs inside Puma and needs its own connections. The comment in database.yml reveals the math:
# Puma threads (3) + SQ worker threads (3) + SQ dispatcher (1) + SQ supervisor (1) = 8
Minutes later:
951b354 -- "Fix R14 memory errors: single-process Puma with in-process Solid Queue"
Heroku Basic dynos have 512 MB of RAM. Forked Puma workers plus a separate Solid Queue worker dyno was exceeding the memory quota (R14 errors). The fix: kill the worker dyno entirely, set WEB_CONCURRENCY=0, and run Solid Queue as an in-process Puma plugin.
The Application-Level Cascade
Three commits in five minutes reveal how deep the damage goes:
0fef54a -- "WLS-212: Fix demo guide left in draft when asset upload fails"
No transaction wrapping meant half-built guides persisted when R2 uploads failed.
e55e5bd -- "WLS-213: Ensure owner contact is created on workspace signup"
The welcome guide was the only code path that created the owner's Contact record. When the guide failed, no contact existed.
8dd06ab -- "WLS-211: Fix trial subscriptions losing Pro entitlements on webhook sync"
Even for users whose subscriptions did get created, Stripe's customer.subscription.created webhook was overwriting Pro trial entitlements with product-level defaults that set features to false.
The Root Cause
The next morning, the deepest root cause finally surfaces:
0d83e24 -- "WLS-215: Fix ActiveStorage checksum error breaking welcome guide creation on signup"
"The aws-sdk-s3 gem (>=1.178.0) sends CRC32 checksums by default, which conflict with ActiveStorage's MD5 checksums on Cloudflare R2."
A Dependabot-bumped gem changed its default checksum algorithm. Cloudflare R2 rejected CRC32 checksums. ActiveStorage uploads silently failed. Everything downstream broke.
The Full Cascade
Reconstructed cause-and-effect chain:
- db:migrate instead of db:prepare -> Solid Queue tables never created
- Solid Queue broken -> async trial subscription jobs never run
- Missing STRIPE_TRIAL_PRICE_ID env var -> even manual subscription creation fails
- Solid Cache pointed at nonexistent Postgres tables -> caching broken
- Solid Cable misconfigured -> real-time updates broken
- aws-sdk-s3 CRC32 default -> Cloudflare R2 rejects uploads
- ActiveStorage uploads silently fail -> welcome guide asset attachment fails
- No transaction wrapping -> half-built guides left in draft
- Contact creation coupled to guide -> owner has no Contact record
- Database pool sized too small -> Solid Queue starved of connections
- Forked Puma workers on 512 MB dyno -> R14 memory errors
- Stripe webhook overwrites trial entitlements -> Pro features show as locked
14 commits to untangle. Zero reverts. Every fix moved forward.
Act VI: Recovery and Features
Healing
The days after the fire focus on repair and resilience:
- e75b0c9 -- Rake task to repair missing welcome guide media blobs for accounts created before the checksum fix
- 26cfb1b -- Fix enrollment emails that weren't being sent on manual enrollment
- a1ce6a7 -- CSP style-src-attr directive (modern browsers treat it separately)
- 822db52 -- Strip possessive 's from slugs
Two commits on WLS-218 and WLS-219 are notable because they're authored directly by Claude (not co-authored) -- the AI agent working independently on its own feature branches:
aeca1de -- "WLS-218: Fix enrollment emails and add weekly reminders" -- Author: Claude
bb85ede -- "WLS-219: Fix broken CTA links in enrollment emails" -- Author: Claude
Account Deletion (WLS-214)
The most complex feature post-fire is account deletion -- the most dangerous operation in any SaaS:
5fb4e94 -- "WLS-214: Add full account deletion with owned workspaces"
Two new services (AccountDeletionService, IdentityDeletionService), a new Settings tab, Stimulus controller for email confirmation, and full test coverage. Then reality bites during manual testing:
- Three bugs -- MediaAsset FK constraint violation, Stripe raising error on nonexistent subscriptions, Turbo Frame lazy loading broken in dialogs
- Another missing FK (poster_asset_id), no transaction wrapping, deliberate choice to put Stripe cancellation before the DB transaction
- After deletion destroys the session, Turbo tries to handle the redirect as a stream. Fix: data-turbo=false
- A 201-line integration test covering the entire cascade from identity through ActiveStorage blobs
Deletion cascades are always harder than they look.
Act VII: The Polish Sprint and Launch (February 6-8)
February 6 Evening: 12 Commits
On February 6, a focused UX polish session produces:
What Changed
- Fix step checkbox vertical alignment
- Remove pl-1 padding from step text
- Add lightbox zoom for step images
- Merge wls243
- Route exceptions through ErrorsController
- Merge wls245
- Fix spacing regression from merge
- Auto-expand next section on completion
- Merge wls248
- Video lightbox controller
- Apply video lightbox to enrollment
- Fix theme CSS cascade vs Tailwind
The few second gaps between merges indicate pre-prepared parallel branches being landed assembly-line style.
The WLS-248 error pages commit deserves attention: it replaces 4 static HTML files (540 lines deleted) with an 83-line ErrorsController and branded error pages. Production had been showing default unstyled Rails error pages.
February 7: The Documentation Drop
The largest single commit after the initial codebase:
4077a03 -- "Add /docs/ documentation section with 29 pages and Pagefind search"
44 files changed, 2,236 insertions
29 pages of user-facing product documentation, a three-column layout (sidebar nav, article content, table of contents), and Pagefind full-text search. Built with Sitepress.
February 8: The Final Polish
The last day brings three notable commits:
b193726 -- "Add US English spelling rule to CLAUDE.md"
Then its enforcement:
044a27b -- "WLS-249: Convert all UK English spellings to US English across UI strings"
32 files changed, 103 insertions(+), 103 deletions(-)
A perfectly balanced find-and-replace: "colour" -> "color", "customise" -> "customize", "enrolment" -> "enrollment" across views, docs, marketing pages, mailers, JavaScript controllers, and test fixtures. Legal pages explicitly exempted.
The UK/US English inconsistency likely reflects the natural drift of a non-native English speaker; the rule and enforcement commit suggest a deliberate decision to standardize for an American market.
The final commit on main:
133741832 -- "Merge branch 'wls249'" (Feb 8)
The Numbers
Timeline
Period | Dates | Commits | Focus
Bootstrap | Dec 15 | 21 | Foundation: Rails, auth, multi-tenancy
Feature Blitz | Dec 16-24 | 226 | Domain models, editor, media, enrollment
Christmas Break | Dec 25-28 | 0 | Silence
January Push | Dec 29 - Jan 28 | 277 | QA, billing, themes, agent workflow
Heroku + Marketing | Jan 29 - Feb 2 | 91 | Deployment, marketing, pre-launch
Production Fire | Feb 3-4 | 80 | Firefighting + feature merges
Recovery + Polish | Feb 5-8 | 18 | Deletion, docs, standardization
Bootstrap | Dec 15 | 21 | Foundation: Rails, auth, multi-tenancy
Feature Blitz | Dec 16-24 | 226 | Domain models, editor, media, enrollment
Christmas Break | Dec 25-28 | 0 | Silence
January Push | Dec 29 - Jan 28 | 277 | QA, billing, themes, agent workflow
Heroku + Marketing | Jan 29 - Feb 2 | 91 | Deployment, marketing, pre-launch
Production Fire | Feb 3-4 | 80 | Firefighting + feature merges
Recovery + Polish | Feb 5-8 | 18 | Deletion, docs, standardization
Commit Velocity
Metric | Value
Total commits (all branches) | 713
Total days with commits | 40 out of 55 calendar days
Average commits per active day | 17.8
Peak day | Feb 3 (67 commits)
Longest streak | Dec 15-24 (10 days)
Longest break | Dec 25-28 (4 days, Christmas)
Migrations | 43
Feature branches | 22+
Total commits (all branches) | 713
Total days with commits | 40 out of 55 calendar days
Average commits per active day | 17.8
Peak day | Feb 3 (67 commits)
Longest streak | Dec 15-24 (10 days)
Longest break | Dec 25-28 (4 days, Christmas)
Migrations | 43
Feature branches | 22+
Major Architectural Decisions
# | Decision | When | Commit
1 | Rails 8.1.1 + Hotwire + Tailwind v4 | Dec 15 | b8d7566
2 | Multi-tenancy via acts_as_tenant | Dec 16 | 4a0675a
3 | SQLite -> PostgreSQL | Dec 19 | bc77c9c
4 | Hub -> Guide (rename) | Dec 22 | 5be3686
5 | Journey -> Enrollment (terminology) | Dec 23 | 479aa4e
6 | /w/ prefix for workspace URLs | Dec 23 | 51043a9
7 | Pundit for authorization | Dec 18 | 3345c4c
8 | Stripe billing with entitlements | Jan 22 | 097b0c7
9 | Theme: guide-level -> workspace-level | Jan 28 | 0fd2a2c
10 | Kamal -> Heroku deployment | Jan 29 | e071a9d
11 | localStorage -> DB auto-save (editor) | Jan 25 | 56b2d20
12 | Stripe Pricing Table -> custom DB-backed | Feb 2 | 38a73b5
13 | Multi-process -> single-process Puma | Feb 4 | 951b354
14 | Solid Cache: Postgres -> SQLite | Feb 3 | c486b81
15 | UK English -> US English | Feb 8 | 044a27b
1 | Rails 8.1.1 + Hotwire + Tailwind v4 | Dec 15 | b8d7566
2 | Multi-tenancy via acts_as_tenant | Dec 16 | 4a0675a
3 | SQLite -> PostgreSQL | Dec 19 | bc77c9c
4 | Hub -> Guide (rename) | Dec 22 | 5be3686
5 | Journey -> Enrollment (terminology) | Dec 23 | 479aa4e
6 | /w/ prefix for workspace URLs | Dec 23 | 51043a9
7 | Pundit for authorization | Dec 18 | 3345c4c
8 | Stripe billing with entitlements | Jan 22 | 097b0c7
9 | Theme: guide-level -> workspace-level | Jan 28 | 0fd2a2c
10 | Kamal -> Heroku deployment | Jan 29 | e071a9d
11 | localStorage -> DB auto-save (editor) | Jan 25 | 56b2d20
12 | Stripe Pricing Table -> custom DB-backed | Feb 2 | 38a73b5
13 | Multi-process -> single-process Puma | Feb 4 | 951b354
14 | Solid Cache: Postgres -> SQLite | Feb 3 | c486b81
15 | UK English -> US English | Feb 8 | 044a27b
Ticket ID Evolution
The project's issue tracking evolved through three systems:
- bd-XXX (Dec 15 only) -- Hex-coded bootstrap tickets for the architectural foundation
- onboardinghub-XXXX (Dec 15 - Jan 4) -- Random hash IDs, likely from an earlier project tracker
- WLS-XXX (Jan 11 onwards) -- Sequential Linear tickets under "WLS Labs" team
Epilogue: What the Git History Reveals
A Rebuild, Not a Greenfield
The sidebar commit on Dec 16 ("Redesign sidebar to match original Replit app") and the architecture-first approach confirm this: OnboardingHub is a Rails rewrite of an existing application, likely a Node/React app built on Replit. The architecture document was reverse-engineered from a working product, the domain model was known in advance, and the UI had a reference implementation to match.
This explains the extraordinary speed of development. When you know exactly what you're building, you can focus purely on execution.
AI as a Force Multiplier
Every commit from Dec 15 through early February is co-authored with Claude Opus 4.5. The AI doesn't just write code -- it writes tests, follows the architecture document, references specific sections, and maintains consistent commit message formatting. The January 17 infrastructure for parallel AI agent sessions shows the developer leaning further into AI-assisted development, eventually reaching the point where Claude authors commits independently.
The transition from Claude Opus 4.5 to 4.6 happens mid-project, and the newer model handles the polish and documentation phase.
The Production Fire as a Rite of Passage
The February 3-4 incident is the defining moment. A single misconfiguration (db:migrate vs db:prepare) exposed a cascade of 12 interconnected failures, each masked by the one before it. The investigation pattern -- fix the surface symptom, discover a deeper issue, fix that, repeat -- is a universal experience in software engineering.
What's notable is the response: no rollbacks, no reverts, no shortcuts. Every fix was a forward-moving commit. The process fixes (documenting env vars, adding reconciliation jobs) show lessons being codified immediately, not deferred.
The Pace Is Unsustainable and They Know It
270 commits in December. 277 in January. The Christmas break is the only real pause. Commit messages deteriorate under pressure: "fixed broken shit", "bunch of changes to sales page", "ignore", "stripe keys". These aren't laziness -- they're exhaustion markers in an otherwise disciplined commit history.
The project's last week (Feb 3-8) averages 25 commits per day. The final 24 hours produce documentation, spelling standardization, and dependency updates -- the work of a project being prepared for handoff or launch, not active feature development.
What's Missing
The git history tells us what was built, but some decisions are invisible:
- Why Rails? -- The rewrite from a JS/React app to Rails is the biggest decision, and it predates the repository
- Why Heroku over Kamal? -- The pivot happens without explanation in the commit messages
- Why Cloudflare R2? -- The initial setup was GCS/S3, but production uses R2. The switch isn't captured in any commit
- Why the rename from Hub to Guide? -- Seven fix commits, but no commit explains why the terminology changed
- Who are the users? -- The marketing pages, welcome guides, and legal pages (WLS Labs Ltd) paint a picture of a real product targeting real customers, but the git history doesn't reveal who they are
First commit: December 15, 2025 at 15:47:17 UTC+8
Last commit on main: February 8, 2026 at 00:31:40 UTC+8
Total commits: 713 across all branches
Total calendar days: 55