Add callouts to LocallyGrown series for enhanced readability
Applied strategic callouts across all 4 posts in the LocallyGrown series: - Info, success, warning, danger, tip, quote, and example callouts - Improved visual hierarchy and information highlighting - Better mobile readability with structured content blocks 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
185
assets/css/_callouts.scss
Normal file
185
assets/css/_callouts.scss
Normal file
@@ -0,0 +1,185 @@
|
||||
// Callout blocks for important information
|
||||
.callout {
|
||||
margin: 1.5rem 0;
|
||||
padding: 0;
|
||||
border-radius: 0.5rem;
|
||||
border-left: 4px solid;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
|
||||
.callout-title {
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
|
||||
.callout-icon {
|
||||
font-size: 1.2em;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.callout-content {
|
||||
padding: 1rem;
|
||||
|
||||
> *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
> *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin: 0.5rem 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
padding: 0.125rem 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note - Default
|
||||
.callout-note {
|
||||
border-left-color: #6b7280;
|
||||
background-color: rgba(107, 114, 128, 0.1);
|
||||
|
||||
.callout-title {
|
||||
color: #d1d5db;
|
||||
background-color: rgba(107, 114, 128, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
// Tip
|
||||
.callout-tip {
|
||||
border-left-color: #fbbf24;
|
||||
background-color: rgba(251, 191, 36, 0.1);
|
||||
|
||||
.callout-title {
|
||||
color: #fef3c7;
|
||||
background-color: rgba(251, 191, 36, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
// Info
|
||||
.callout-info {
|
||||
border-left-color: #60a5fa;
|
||||
background-color: rgba(96, 165, 250, 0.1);
|
||||
|
||||
.callout-title {
|
||||
color: #dbeafe;
|
||||
background-color: rgba(96, 165, 250, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
// Warning
|
||||
.callout-warning {
|
||||
border-left-color: #fb923c;
|
||||
background-color: rgba(251, 146, 60, 0.1);
|
||||
|
||||
.callout-title {
|
||||
color: #fed7aa;
|
||||
background-color: rgba(251, 146, 60, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
// Danger
|
||||
.callout-danger {
|
||||
border-left-color: #f87171;
|
||||
background-color: rgba(248, 113, 113, 0.1);
|
||||
|
||||
.callout-title {
|
||||
color: #fecaca;
|
||||
background-color: rgba(248, 113, 113, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
// Success
|
||||
.callout-success {
|
||||
border-left-color: #4ade80;
|
||||
background-color: rgba(74, 222, 128, 0.1);
|
||||
|
||||
.callout-title {
|
||||
color: #d1fae5;
|
||||
background-color: rgba(74, 222, 128, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
// Question
|
||||
.callout-question {
|
||||
border-left-color: #c084fc;
|
||||
background-color: rgba(192, 132, 252, 0.1);
|
||||
|
||||
.callout-title {
|
||||
color: #e9d5ff;
|
||||
background-color: rgba(192, 132, 252, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
// Quote
|
||||
.callout-quote {
|
||||
border-left-color: #94a3b8;
|
||||
background-color: rgba(148, 163, 184, 0.1);
|
||||
font-style: italic;
|
||||
|
||||
.callout-title {
|
||||
color: #e2e8f0;
|
||||
background-color: rgba(148, 163, 184, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
// Example
|
||||
.callout-example {
|
||||
border-left-color: #06b6d4;
|
||||
background-color: rgba(6, 182, 212, 0.1);
|
||||
|
||||
.callout-title {
|
||||
color: #cffafe;
|
||||
background-color: rgba(6, 182, 212, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
// Bug
|
||||
.callout-bug {
|
||||
border-left-color: #dc2626;
|
||||
background-color: rgba(220, 38, 38, 0.1);
|
||||
|
||||
.callout-title {
|
||||
color: #fee2e2;
|
||||
background-color: rgba(220, 38, 38, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 640px) {
|
||||
.callout {
|
||||
margin: 1rem 0;
|
||||
border-radius: 0.375rem;
|
||||
|
||||
.callout-title {
|
||||
padding: 0.625rem 0.875rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.callout-content {
|
||||
padding: 0.875rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
}
|
@@ -13,4 +13,7 @@
|
||||
overflow-x: hidden !important;
|
||||
min-height: unset !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Import callout styles
|
||||
@import 'callouts';
|
@@ -18,7 +18,15 @@ _How a failed wholesale experiment in Athens, Georgia became the foundation for
|
||||
|
||||
## TL;DR
|
||||
|
||||
What started as a failed restaurant wholesale cooperative in Athens, GA accidentally became what I believe was the world's first online farmers market platform. A simple innovation—letting customers pre-order exactly what they wanted before farmers harvested—addressed persistent problems that everyone in local food systems knew about but had struggled to solve with traditional farmers markets and CSAs. By 2010-2011, the Rails-powered platform served 100+ markets across North America through a sustainable 3% commission model. Our Athens market alone had 60 growers and 1,500 regular customers, processing $10K+ weekly. The architectural decisions made during intense 4 AM development sessions would shape the platform for the next 19 years.
|
||||
{{< callout type="info" title="The LocallyGrown Story in Brief" >}}
|
||||
What started as a failed restaurant wholesale cooperative in Athens, GA accidentally became what I believe was the world's first online farmers market platform. A simple innovation—letting customers pre-order exactly what they wanted before farmers harvested—addressed persistent problems that everyone in local food systems knew about but had struggled to solve with traditional farmers markets and CSAs.
|
||||
|
||||
By 2010-2011:
|
||||
- The Rails-powered platform served 100+ markets across North America
|
||||
- Sustainable 3% commission model
|
||||
- Athens market alone: 60 growers, 1,500 regular customers, $10K+ weekly
|
||||
- Architectural decisions made during intense 4 AM development sessions would shape the platform for the next 19 years
|
||||
{{< /callout >}}
|
||||
|
||||
---
|
||||
|
||||
@@ -73,14 +81,14 @@ By focusing on individual customers instead of restaurants, we stumbled onto a m
|
||||
|
||||
The innovation wasn't recognizing these problems existed—farmers, customers, and market organizers had been wrestling with these trade-offs for decades. CSAs were one attempt at a solution, buying clubs were another, direct sales were yet another. Each approach solved some problems while creating others, forcing people to choose the least-bad option for their situation. What we stumbled onto was a way to get the benefits of all three models while avoiding most of their downsides.
|
||||
|
||||
> **Predictable Harvesting 101**
|
||||
>
|
||||
> - **Friday-Sunday:** Growers list what they expect to be able to harvest
|
||||
> - **Monday-Tuesday:** Customers place orders for exactly what they want
|
||||
> - **Wednesday:** Growers harvest knowing every item is presold
|
||||
> - **Thursday:** Coordinated drop-off and pickup
|
||||
>
|
||||
> _This weekly cycle was liberating in 2002: imagine harvesting your produce already knowing that everything has been sold._
|
||||
{{< callout type="success" title="Predictable Harvesting 101" >}}
|
||||
- **Friday-Sunday:** Growers list what they expect to be able to harvest
|
||||
- **Monday-Tuesday:** Customers place orders for exactly what they want
|
||||
- **Wednesday:** Growers harvest knowing every item is presold
|
||||
- **Thursday:** Coordinated drop-off and pickup
|
||||
|
||||
_This weekly cycle was liberating in 2002: imagine harvesting your produce already knowing that everything has been sold._
|
||||
{{< /callout >}}
|
||||
|
||||
That structure didn’t just reduce waste—it made new products possible. One grower with a single kefir lime tree could finally offer leaves for sale. At the traditional market, they often went unsold and spoiled, and he stopped bringing them. In our system, he could list five, sell two, and leave the rest on the tree. It was an unintended side effect: a much broader range of products became available to customers.
|
||||
|
||||
@@ -135,13 +143,13 @@ end
|
||||
|
||||
This simple method enabled `athens.locallygrown.net`, `madison.locallygrown.net`, and dozens of other markets to operate independently while sharing the same codebase. Each market could have its own branding, managers, growers, and customers, all isolated by subdomain.
|
||||
|
||||
> **Why Subdomains in 2006?**
|
||||
>
|
||||
> - **Isolation:** Each market's data completely separated
|
||||
> - **Branding:** Markets could customize domains and appearance
|
||||
> - **Scaling:** Single codebase serving multiple communities
|
||||
> - **Trust:** Local URLs for local communities
|
||||
> - **Technical simplicity:** One application, many markets
|
||||
{{< callout type="example" title="Why Subdomains in 2006?" >}}
|
||||
- **Isolation:** Each market's data completely separated
|
||||
- **Branding:** Markets could customize domains and appearance
|
||||
- **Scaling:** Single codebase serving multiple communities
|
||||
- **Trust:** Local URLs for local communities
|
||||
- **Technical simplicity:** One application, many markets
|
||||
{{< /callout >}}
|
||||
|
||||
**Full HTML/CSS/JS Customization System:**
|
||||
Perhaps my most innovative and ultimately problematic decision was giving markets complete control over their appearance. Markets could inject custom HTML, CSS, and JavaScript into their pages. This was above and beyond what was offered by most platforms in 2006.
|
||||
@@ -163,11 +171,13 @@ For five years, the Rails platform hummed along perfectly. I migrated through Ra
|
||||
|
||||
**The Numbers That Proved the Model**
|
||||
|
||||
{{< callout type="success" title="The Numbers That Proved the Model" >}}
|
||||
By 2010-2011, when I was giving presentations about LocallyGrown, the Athens market alone had grown to:
|
||||
|
||||
- **60 active growers** using the platform
|
||||
- **1,500 regular customers** placing 200 or more weekly orders
|
||||
- **$10,000–12,000 in weekly sales** flowing through the system
|
||||
{{< /callout >}}
|
||||
|
||||
But the real validation came from replication. The platform expanded to support **50+ markets processing orders across North America** (and then 75, 100, and more), with **10 more markets getting started** at any given time. Each market operated independently with its own subdomain, branding, and community, but shared the same robust technical infrastructure.
|
||||
|
||||
@@ -202,7 +212,9 @@ By 2011, LocallyGrown.net had grown from a failed wholesale experiment to a thri
|
||||
|
||||
The weekly cycle that started as an accidental discovery had become the heartbeat of dozens of food communities. Growers could plan their harvests with confidence. Customers could access fresh, local food with online convenience. Market managers could run sophisticated operations without technical expertise.
|
||||
|
||||
> "We had accidentally solved problems that traditional farmers markets, CSAs, and buying clubs couldn't address—and built the technical infrastructure to scale that solution nationwide."
|
||||
{{< callout type="quote" >}}
|
||||
"We had accidentally solved problems that traditional farmers markets, CSAs, and buying clubs couldn't address—and built the technical infrastructure to scale that solution nationwide."
|
||||
{{< /callout >}}
|
||||
|
||||
The platform was stable, profitable in a sustainable way, and serving a real need in communities across the country. Rails 3.0.20 was working perfectly. The customization system was our competitive advantage. The multi-market architecture was battle-tested and scalable.
|
||||
|
||||
|
@@ -20,7 +20,22 @@ _How a platform serving thousands of farmers and customers nearly died from its
|
||||
|
||||
## TL;DR
|
||||
|
||||
By 2025, the LocallyGrown.net platform was dying from technical extinction. Running Rails 3.0.20 and Ruby 2.0.0 (both over a decade obsolete), it couldn't be rebuilt if servers failed—Ruby 2.0.0 won't compile on modern systems. Multiple Rails upgrade attempts had failed due to the customization system that once made me competitive. COVID created a cruel irony: maximum demand for online farmers market infrastructure meeting maximum technical constraints. After closing my own Athens market in 2021 and watching passion-driven competitors win markets with modern platforms, I faced three choices: help migrate users to competitors, shut it down, or rebuild everything from scratch while working around a day job. Failure would have meant thousands of farmers and customers losing their platform overnight. I chose to give it my all: 1,865 commits over six months, complete Rails-to-SvelteKit migration, preserving 23 years of agricultural innovation and the communities that depended on it.
|
||||
{{< callout type="danger" title="Platform in Critical Condition" >}}
|
||||
By 2025, the LocallyGrown.net platform was dying from technical extinction:
|
||||
- Running Rails 3.0.20 and Ruby 2.0.0 (both over a decade obsolete)
|
||||
- **Critical:** Ruby 2.0.0 won't compile on modern systems - couldn't rebuild if servers failed
|
||||
- Multiple Rails upgrade attempts had failed due to the customization system
|
||||
- COVID created a cruel irony: maximum demand meeting maximum technical constraints
|
||||
|
||||
After closing my own Athens market in 2021 and watching passion-driven competitors win markets with modern platforms, I faced three choices:
|
||||
1. Help migrate users to competitors
|
||||
2. Shut it down
|
||||
3. Rebuild everything from scratch while working around a day job
|
||||
|
||||
Failure would have meant thousands of farmers and customers losing their platform overnight.
|
||||
|
||||
**Result:** 1,865 commits over six months, complete Rails-to-SvelteKit migration, preserving 23 years of agricultural innovation.
|
||||
{{< /callout >}}
|
||||
|
||||
---
|
||||
|
||||
@@ -32,7 +47,9 @@ This has become routine. I keep a laptop within arm's reach no matter what I'm d
|
||||
|
||||
If it wasn't spam bots overwhelming the session system, it was something else. A gem broke, a server ran out of memory, a scanner found a new exploit—there was always something that could take the site down for hours, or longer.
|
||||
|
||||
> "I kept a laptop within arm's reach no matter what I was doing, just in case. The constant vigilance was draining everything."
|
||||
{{< callout type="quote" >}}
|
||||
"I kept a laptop within arm's reach no matter what I was doing, just in case. The constant vigilance was draining everything."
|
||||
{{< /callout >}}
|
||||
|
||||
The inevitable truth was becoming clear: one day, the bots would win. Or something worse would. And when that day came, 23 years of agricultural innovation would simply vanish.
|
||||
|
||||
@@ -50,19 +67,19 @@ This wasn't due to neglect or lack of awareness. I was acutely conscious of the
|
||||
|
||||
Each new Rails version represented not just an upgrade, but a potential breaking change to the complex customization system that markets depended on. The architectural decisions that had enabled rapid growth in 2006—deep Rails integration, custom HTML/CSS/JS injection, complex ActiveRecord relationships—became barriers to modernization.
|
||||
|
||||
> **Artifacts of Failed Upgrades**
|
||||
>
|
||||
> ```bash
|
||||
> git branch -a | grep upgrade
|
||||
> rails-4-upgrade-attempt-2015
|
||||
> rails-4-second-try-2016
|
||||
> rails-5-api-experiment-2017
|
||||
>
|
||||
> git log rails-4-upgrade-attempt-2015 --oneline | wc -l
|
||||
> 47 commits over 6 months of weekend work
|
||||
>
|
||||
> Final commit message: "Customization system fundamentally incompatible with Rails 4 asset pipeline."
|
||||
> ```
|
||||
{{< callout type="warning" title="Artifacts of Failed Upgrades" >}}
|
||||
```bash
|
||||
git branch -a | grep upgrade
|
||||
rails-4-upgrade-attempt-2015
|
||||
rails-4-second-try-2016
|
||||
rails-5-api-experiment-2017
|
||||
|
||||
git log rails-4-upgrade-attempt-2015 --oneline | wc -l
|
||||
47 commits over 6 months of weekend work
|
||||
|
||||
Final commit message: "Customization system fundamentally incompatible with Rails 4 asset pipeline."
|
||||
```
|
||||
{{< /callout >}}
|
||||
|
||||
When Rails 5 introduced API capabilities, I saw a potential path forward. I experimented with creating a business logic API layer from my Rails 3 code, which would allow me to build a new frontend free from the constraints I was under. The approach showed promise (and would later become the foundation for my SvelteKit migration), but required more sustained development time and financial resources than I could afford to give it.
|
||||
|
||||
@@ -114,13 +131,13 @@ What started as postponed upgrades became architectural shackles:
|
||||
|
||||
**Customization Albatross:** The HTML/CSS/JS injection system that was innovative in 2006 was preventing mobile optimization in 2020. While competitors launched mobile-first platforms, LocallyGrown markets looked dated and functioned poorly on smartphones.
|
||||
|
||||
> **Why "Can't Rebuild" Means Extinct**
|
||||
>
|
||||
> - Ruby 2.0.0 fails to compile on Ubuntu 22.04+ and macOS 14+
|
||||
> - Rails 3.0.20 has dozens of known CVEs with no available fixes
|
||||
> - Session attacks regularly generate 500K+ files, exhausting filesystem inodes
|
||||
> - 12 critical gems abandoned with no Ruby 2.0-compatible updates
|
||||
> - Development environment setup requires VMs running decade-old OS versions
|
||||
{{< callout type="danger" title="Why 'Can't Rebuild' Means Extinct" >}}
|
||||
- Ruby 2.0.0 fails to compile on Ubuntu 22.04+ and macOS 14+
|
||||
- Rails 3.0.20 has dozens of known CVEs with no available fixes
|
||||
- Session attacks regularly generate 500K+ files, exhausting filesystem inodes
|
||||
- 12 critical gems abandoned with no Ruby 2.0-compatible updates
|
||||
- Development environment setup requires VMs running decade-old OS versions
|
||||
{{< /callout >}}
|
||||
|
||||
---
|
||||
|
||||
@@ -128,12 +145,12 @@ What started as postponed upgrades became architectural shackles:
|
||||
|
||||
By January 2025, I faced a stark reality: the system that had worked for 19 years was about to become extinct. Not broken—extinct. I had no clear path forward to ever upgrading my system. The platform was living on borrowed time, held together by manual interventions, emergency patches, and the constant stress of knowing that the next alert could be the one I couldn't fix.
|
||||
|
||||
The numbers were stark:
|
||||
|
||||
{{< callout type="info" title="The Stakes" >}}
|
||||
- **Dozens of markets** depending on the platform for their entire online presence
|
||||
- **Hundreds of growers** using the system for their livelihoods
|
||||
- **Thousands of customers** relying on it for local food access
|
||||
- **23 years of data**, relationships, and business processes that couldn't be easily migrated anywhere else
|
||||
{{< /callout >}}
|
||||
|
||||
### The Competition Had Evolved
|
||||
|
||||
@@ -161,11 +178,11 @@ The irony was stark: I had the knowledge to market the platform aggressively, re
|
||||
|
||||
## The Ultimatum: Modernize or Die
|
||||
|
||||
I had three choices:
|
||||
|
||||
{{< callout type="question" title="The Ultimatum" >}}
|
||||
1. **Migrate to a competitor** - Abandon 23 years of custom business logic and force all markets to start over
|
||||
2. **Shut down the platform** - Let the infrastructure fail and walk away from my life's work
|
||||
3. **Complete modernization** - Rebuild everything from scratch while keeping the business running
|
||||
{{< /callout >}}
|
||||
|
||||
The first two options meant losing everything that made LocallyGrown unique. The complex pricing logic, the multi-market architecture, the grower-customer relationships, the historical data—all of it would be gone. Every row represented growers, customers, and years of trust. Losing that wasn’t an option.
|
||||
|
||||
@@ -189,14 +206,14 @@ In my day job as VP of Technology at Infinity Interactive, a full-service softwa
|
||||
|
||||
I was out of touch with modern Rails and couldn’t tell you how Rails 8 compares to the world I knew. But I knew that if I wanted to get this job done at all, I'd have to go with the framework I knew and loved best: Svelte 5. The Rails 5 API experiments from 2017 had already taught me that separating business logic from presentation was the path forward, and SvelteKit was the perfect modern complement to that approach.
|
||||
|
||||
**My Constraints**
|
||||
|
||||
{{< callout type="example" title="My Constraints" >}}
|
||||
- **Timeline:** six months, part-time
|
||||
- **Keep running:** production Rails stays up
|
||||
- **Must keep:** all business logic, subdomains
|
||||
- **Fix:** mobile UX, unsafe customization
|
||||
- **Ideals:** no password resets, minimal downtime, no schema changes
|
||||
- **Success:** zero data loss, feature parity, faster
|
||||
{{< /callout >}}
|
||||
|
||||
This series is the story of how I did it: **1,865 commits over six months** of part-time development, a complete Rails 3.0.20 to SvelteKit migration, three weeks of crisis management when everything went wrong, and the lessons learned from rescuing a platform from technological extinction. Every commit felt like a race against the clock.
|
||||
|
||||
|
@@ -21,16 +21,20 @@ slug: locallygrown-rails-svelte-migration
|
||||
|
||||
_How I migrated decades of complex ActiveRecord relationships, subdomain multitenancy, and business logic to a modern stack without losing a single feature_
|
||||
|
||||
> **Note on Code Examples:** All code samples in this post are simplified, illustrative examples to demonstrate concepts, and not actual production code. They're edited for clarity and brevity. Please don't point out all the bugs! 😊
|
||||
{{< callout type="note" title="Note on Code Examples" >}}
|
||||
All code samples in this post are simplified, illustrative examples to demonstrate concepts, and not actual production code. They're edited for clarity and brevity. Please don't point out all the bugs! 😊
|
||||
{{< /callout >}}
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
* Rebuilt 19 years of Rails in SvelteKit while production stayed live.
|
||||
* TypeScript domain model + service layer, Drizzle ORM, subdomain multitenancy, role-based auth, secure theming.
|
||||
* Zero data transformation; two-month preview; 2 AM, all-or-nothing cutover.
|
||||
* 1,865 commits → feature parity.
|
||||
{{< callout type="info" title="The Migration in Brief" >}}
|
||||
- Rebuilt 19 years of Rails in SvelteKit while production stayed live
|
||||
- TypeScript domain model + service layer, Drizzle ORM, subdomain multitenancy, role-based auth, secure theming
|
||||
- Zero data transformation; two-month preview; 2 AM, all-or-nothing cutover
|
||||
- 1,865 commits → feature parity
|
||||
{{< /callout >}}
|
||||
|
||||
---
|
||||
|
||||
@@ -190,8 +194,9 @@ This separation made testing easier and removed framework dependencies from busi
|
||||
|
||||
_Discover why keeping the messy 19-year-old database schema was the smartest decision I made._
|
||||
|
||||
> **Why I Didn't Redesign the Database (And Why That Saved Me)**
|
||||
> The existing schema had every antipattern in the book: denormalized data, legacy columns, Rails-specific naming. But keeping it meant zero data transformation, instant production testing, and eliminating an entire category of migration failures. When you're doing an all-or-nothing cutover at 2 AM, fewer moving parts is everything.
|
||||
{{< callout type="tip" title="Why I Didn't Redesign the Database" >}}
|
||||
The existing schema had every antipattern in the book: denormalized data, legacy columns, Rails-specific naming. But keeping it meant zero data transformation, instant production testing, and eliminating an entire category of migration failures. When you're doing an all-or-nothing cutover at 2 AM, fewer moving parts is everything.
|
||||
{{< /callout >}}
|
||||
|
||||
When starting the migration, I faced a fundamental choice: design a clean, modern database schema optimized for SvelteKit, or keep the existing Rails-era schema with all its quirks and compromises from decades of evolution.
|
||||
|
||||
@@ -475,7 +480,9 @@ export async function load({ locals }) {
|
||||
{/if}
|
||||
```
|
||||
|
||||
> **Note:** Permissions are computed server-side and hydrated into the page data; the client never decides authority. This ensures business logic stays on the backend and prevents client-side authorization bypasses.
|
||||
{{< callout type="warning" title="Security Note" >}}
|
||||
Permissions are computed server-side and hydrated into the page data; the client never decides authority. This ensures business logic stays on the backend and prevents client-side authorization bypasses.
|
||||
{{< /callout >}}
|
||||
|
||||
**Outcome:** Authorization logic moved from scattered checks to a single source of truth.
|
||||
|
||||
@@ -693,7 +700,8 @@ Once MySQL 8 went live, Rails 3 could never connect again. Once the new system w
|
||||
|
||||
### The Migration Moment
|
||||
|
||||
August 14, 2025, 2 AM: The most nerve-wracking deployment of my career (45 minutes migration + 10 minutes verification + two whole hours of DNS propagation):
|
||||
{{< callout type="danger" title="August 14, 2025, 2 AM" >}}
|
||||
The most nerve-wracking deployment of my career (45 minutes migration + 10 minutes verification + two whole hours of DNS propagation):
|
||||
|
||||
1. **2:00 AM:** Put Rails in maintenance mode
|
||||
2. **2:05 AM:** Final backup of MySQL 5.5 database
|
||||
@@ -702,6 +710,7 @@ August 14, 2025, 2 AM: The most nerve-wracking deployment of my career (45 minut
|
||||
5. **3:00 AM:** Update DNS to point to SvelteKit
|
||||
6. **3:05 AM:** Remove maintenance mode
|
||||
7. **5:00 AM:** Start breathing again
|
||||
{{< /callout >}}
|
||||
|
||||
After two months of preview testing, when August 14 arrived, every market, every grower, every customer switched simultaneously.
|
||||
|
||||
@@ -903,13 +912,13 @@ The preview period helped identify workflow issues, UI confusion points, and mis
|
||||
|
||||
**Building Trust Through Transparency:**
|
||||
|
||||
{{< callout type="quote" title="Building Trust Through Transparency" >}}
|
||||
From my June announcement:
|
||||
|
||||
> "I'm taking extra time to ensure that all the custom styling and themes that markets have developed over the years are properly preserved and working beautifully on the new platform."
|
||||
"I'm taking extra time to ensure that all the custom styling and themes that markets have developed over the years are properly preserved and working beautifully on the new platform."
|
||||
|
||||
From the July 31 update:
|
||||
|
||||
> "I've been working on this transition literally every day for over four months now, and I'm excited to move forward with the final steps."
|
||||
"I've been working on this transition literally every day for over four months now, and I'm excited to move forward with the final steps."
|
||||
{{< /callout >}}
|
||||
|
||||
The key message throughout: "Your data is safe, and I'm taking every precaution."
|
||||
|
||||
|
@@ -21,13 +21,23 @@ slug: locallygrown-reality-of-production
|
||||
|
||||
_Three thousand passing tests don't mean your system works_
|
||||
|
||||
> **Note:** This is based on actual production issues from August 2025. These were the real bugs that affected real users running real businesses.
|
||||
{{< callout type="note" >}}
|
||||
This is based on actual production issues from August 2025. These were the real bugs that affected real users running real businesses.
|
||||
{{< /callout >}}
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
I launched with 3,000 passing tests, weeks of manual checks on production data, and a two-month beta—and still hit two weeks of real-world bugs. DNS delays, fee miscalculations, role-permission gaps, Stripe edge cases, fragile email templates, and a thousand small cuts. I fixed 314 commits in 18 days and kept markets running. This is what broke, why, and what I’d do differently.
|
||||
{{< callout type="warning" title="The Production Reality Check" >}}
|
||||
I launched with 3,000 passing tests, weeks of manual checks on production data, and a two-month beta—and still hit two weeks of real-world bugs:
|
||||
- DNS delays, fee miscalculations, role-permission gaps
|
||||
- Stripe edge cases, fragile email templates
|
||||
- A thousand small cuts
|
||||
- **Result:** 314 commits in 18 days to keep markets running
|
||||
|
||||
This is what broke, why, and what I'd do differently.
|
||||
{{< /callout >}}
|
||||
|
||||
---
|
||||
|
||||
@@ -175,18 +185,18 @@ This wasn't just a website with bugs. This was dozens of live food systems, hund
|
||||
|
||||
## The Two-Week Bug Avalanche
|
||||
|
||||
{{< callout type="danger" title="The Two-Week Bug Avalanche" >}}
|
||||
**The Numbers:**
|
||||
|
||||
- **314 commits** from August 14 to September 1
|
||||
- **58 pull requests** merged
|
||||
- **105 commits** with "fix", "bug", or "critical" in the first week alone
|
||||
|
||||
Look at the PR names from August 18:
|
||||
|
||||
- "morning bugs"
|
||||
- "afternoon fixes"
|
||||
- "evening bugs"
|
||||
- "night bugs"
|
||||
{{< /callout >}}
|
||||
|
||||
I was naming my branches by time of day because there were too many to give them meaningful names. I was working quickly and fixing things as quickly as they were coming in, but they just kept coming.
|
||||
|
||||
@@ -210,11 +220,13 @@ What kept me going was knowing this wasn't about me. It was about the communitie
|
||||
|
||||
The market managers became my QA team in production. They had every right to be furious. Instead, they sent detailed bug reports with screenshots, exact reproduction steps, and impact assessments.
|
||||
|
||||
{{< callout type="example" title="Evolution of Bug Reports" >}}
|
||||
One manager's emails evolved into increasingly detailed technical reports:
|
||||
|
||||
- Day 1: "The total amount due to growers is displaying incorrectly"
|
||||
- Day 3: "When I export the checks .csv the amounts are correct but the display shows \$85 instead of $73.95 after the 13% fee"
|
||||
- Day 5: "I can replicate this. It only happens when you select specific growers like 3C or BMB unless you also choose honey or baked goods category"
|
||||
- **Day 1:** "The total amount due to growers is displaying incorrectly"
|
||||
- **Day 3:** "When I export the checks .csv the amounts are correct but the display shows \$85 instead of $73.95 after the 13% fee"
|
||||
- **Day 5:** "I can replicate this. It only happens when you select specific growers like 3C or BMB unless you also choose honey or baked goods category"
|
||||
{{< /callout >}}
|
||||
|
||||
They were debugging complex interaction patterns while running physical markets. They discovered edge cases I never would have found. They showed patience I didn't think possible.
|
||||
|
||||
@@ -292,16 +304,17 @@ Pair fast rollback-like mitigations (feature flags, circuit breakers) with rapid
|
||||
|
||||
## Where We Are Now
|
||||
|
||||
{{< callout type="success" title="Four Weeks Later" >}}
|
||||
It's been four weeks since launch. I fixed more bugs last night. Reports are still coming in, but it's a trickle now instead of a flood.
|
||||
|
||||
But here's what matters: The system works. Markets are running. Orders are flowing. Communities are getting their local food.
|
||||
But here's what matters: **The system works.** Markets are running. Orders are flowing. Communities are getting their local food.
|
||||
|
||||
Yes, it was messier than I'd hoped. Yes, I made mistakes. But I also:
|
||||
|
||||
- Successfully migrated 23 years of Rails code to modern infrastructure
|
||||
- Kept dozens of markets running without any data loss
|
||||
- Fixed 314 commits worth of bugs without breaking production
|
||||
- Maintained trust by responding quickly and transparently
|
||||
{{< /callout >}}
|
||||
|
||||
I'm not in crisis management mode anymore. I'm able to think about (and even implement) new features. The system is stable enough that I can look forward, not just fix what's broken.
|
||||
|
||||
|
37
layouts/shortcodes/callout.html
Normal file
37
layouts/shortcodes/callout.html
Normal file
@@ -0,0 +1,37 @@
|
||||
{{- $type := .Get "type" | default "note" -}}
|
||||
{{- $title := .Get "title" | default (title $type) -}}
|
||||
{{- $icon := "" -}}
|
||||
|
||||
{{- if eq $type "note" -}}
|
||||
{{- $icon = "📝" -}}
|
||||
{{- else if eq $type "tip" -}}
|
||||
{{- $icon = "💡" -}}
|
||||
{{- else if eq $type "info" -}}
|
||||
{{- $icon = "ℹ️" -}}
|
||||
{{- else if eq $type "warning" -}}
|
||||
{{- $icon = "⚠️" -}}
|
||||
{{- else if eq $type "danger" -}}
|
||||
{{- $icon = "🚨" -}}
|
||||
{{- else if eq $type "success" -}}
|
||||
{{- $icon = "✅" -}}
|
||||
{{- else if eq $type "question" -}}
|
||||
{{- $icon = "❓" -}}
|
||||
{{- else if eq $type "quote" -}}
|
||||
{{- $icon = "💬" -}}
|
||||
{{- else if eq $type "example" -}}
|
||||
{{- $icon = "📋" -}}
|
||||
{{- else if eq $type "bug" -}}
|
||||
{{- $icon = "🐛" -}}
|
||||
{{- else -}}
|
||||
{{- $icon = "📌" -}}
|
||||
{{- end -}}
|
||||
|
||||
<div class="callout callout-{{ $type }}">
|
||||
<div class="callout-title">
|
||||
<span class="callout-icon">{{ $icon }}</span>
|
||||
<span>{{ $title }}</span>
|
||||
</div>
|
||||
<div class="callout-content">
|
||||
{{ .Inner | markdownify }}
|
||||
</div>
|
||||
</div>
|
Reference in New Issue
Block a user