Compare commits
4 Commits
ef12f8bb05
...
718ffb1638
Author | SHA1 | Date | |
---|---|---|---|
|
718ffb1638 | ||
|
062c54a26b | ||
|
9f77ec7c10 | ||
|
5967340854 |
@@ -7,6 +7,7 @@ theme = "m10c"
|
|||||||
avatar = "face.jpg"
|
avatar = "face.jpg"
|
||||||
description = "Links, thoughts, & what-not from Athens, GA."
|
description = "Links, thoughts, & what-not from Athens, GA."
|
||||||
favicon = "favicon.ico"
|
favicon = "favicon.ico"
|
||||||
|
images = ["/default-og.png"]
|
||||||
[[params.social]]
|
[[params.social]]
|
||||||
icon = "home"
|
icon = "home"
|
||||||
name = "Home"
|
name = "Home"
|
||||||
|
@@ -8,7 +8,7 @@ tags:
|
|||||||
- locallygrown
|
- locallygrown
|
||||||
categories:
|
categories:
|
||||||
- locallygrown
|
- locallygrown
|
||||||
lastmod: 2025-09-09T00:20:46.135Z
|
lastmod: 2025-09-22T20:54:42.989Z
|
||||||
slug: locallygrown-origin-story
|
slug: locallygrown-origin-story
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -235,8 +235,8 @@ _This is part one of a series documenting the creation, evolution, and rescue of
|
|||||||
### The Series
|
### The Series
|
||||||
|
|
||||||
1. **From Accidental Discovery to Agricultural Infrastructure** ← _You are here_
|
1. **From Accidental Discovery to Agricultural Infrastructure** ← _You are here_
|
||||||
2. The 23-Year Rescue Mission: Saving Agricultural Innovation from Technical Extinction
|
2. [The 23-Year Rescue Mission: Saving Agricultural Innovation from Technical Extinction](https://blog.kestrelsnest.social/posts/locallygrown-rescue-mission/)
|
||||||
3. The Architecture Challenge: Translating 19 Years of Rails Logic to Modern SvelteKit
|
3. [The Architecture Challenge: Translating 19 Years of Rails Logic to Modern SvelteKit](https://blog.kestrelsnest.social/posts/locallygrown-rails-svelte-migration/)
|
||||||
4. Crisis Response: When Launch Day Goes Wrong
|
4. [The Reality of Production: When Hope Meets Live Users](https://blog.kestrelsnest.social/posts/locallygrown-reality-of-production/)
|
||||||
5. Lessons from the Solo Developer Using Modern Tools
|
5. [Lessons from the Solo Developer Using Modern Tools](https://blog.kestrelsnest.social/posts/locallygrown-lessons/)
|
||||||
6. The Future: Building on Modern Foundations
|
6. The Future: Building on Modern Foundations
|
||||||
|
@@ -3,12 +3,12 @@ title: "The 23-Year Rescue Mission: Saving Agricultural Innovation from Technica
|
|||||||
description: By 2025, LocallyGrown.net faced extinction on Rails 3. The story of constant firefighting, failed upgrades, and the impossible choice to rebuild or die.
|
description: By 2025, LocallyGrown.net faced extinction on Rails 3. The story of constant firefighting, failed upgrades, and the impossible choice to rebuild or die.
|
||||||
date: 2025-09-09T19:53:40.930Z
|
date: 2025-09-09T19:53:40.930Z
|
||||||
preview: ""
|
preview: ""
|
||||||
draft: true
|
draft: false
|
||||||
tags:
|
tags:
|
||||||
- locallygrown
|
- locallygrown
|
||||||
categories:
|
categories:
|
||||||
- locallygrown
|
- locallygrown
|
||||||
lastmod: 2025-09-09T20:02:11.281Z
|
lastmod: 2025-09-22T20:54:49.481Z
|
||||||
keywords:
|
keywords:
|
||||||
- locallygrown
|
- locallygrown
|
||||||
slug: locallygrown-rescue-mission
|
slug: locallygrown-rescue-mission
|
||||||
@@ -233,7 +233,7 @@ _This is part two of a series documenting the rescue and modernization of Locall
|
|||||||
|
|
||||||
1. [From Accidental Discovery to Agricultural Infrastructure (2002-2011)](https://blog.kestrelsnest.social/posts/locallygrown-origin-story/)
|
1. [From Accidental Discovery to Agricultural Infrastructure (2002-2011)](https://blog.kestrelsnest.social/posts/locallygrown-origin-story/)
|
||||||
2. **The 23-Year Rescue Mission: Saving Agricultural Innovation from Technical Extinction** ← _You are here_
|
2. **The 23-Year Rescue Mission: Saving Agricultural Innovation from Technical Extinction** ← _You are here_
|
||||||
3. The Architecture Challenge: Translating 19 Years of Rails Logic to Modern SvelteKit
|
3. [The Architecture Challenge: Translating 19 Years of Rails Logic to Modern SvelteKit](https://blog.kestrelsnest.social/posts/locallygrown-rails-svelte-migration/)
|
||||||
4. Crisis Response: When Launch Day Goes Wrong
|
4. [The Reality of Production: When Hope Meets Live Users](https://blog.kestrelsnest.social/posts/locallygrown-reality-of-production/)
|
||||||
5. Lessons from the Solo Developer + AI Trenches
|
5. [Lessons from the Solo Developer Using Modern Tools](https://blog.kestrelsnest.social/posts/locallygrown-lessons/)
|
||||||
6. The Future: Building on Modern Foundations
|
6. The Future: Building on Modern Foundations
|
||||||
|
@@ -0,0 +1,979 @@
|
|||||||
|
---
|
||||||
|
title: "The Architecture Challenge: Translating 19 Years of Rails Logic to Modern SvelteKit"
|
||||||
|
description: How I translated a 19-year Rails monolith to SvelteKit—TypeScript services, Drizzle, subdomain multitenancy, role-based auth, secure theming — without downtime.
|
||||||
|
date: 2025-09-15T01:44:05.496Z
|
||||||
|
preview: ""
|
||||||
|
draft: false
|
||||||
|
tags:
|
||||||
|
- locallygrown
|
||||||
|
- svelte
|
||||||
|
- sveltekit
|
||||||
|
- rails
|
||||||
|
categories:
|
||||||
|
- locallygrown
|
||||||
|
lastmod: 2025-09-22T20:54:54.910Z
|
||||||
|
keywords:
|
||||||
|
- locallygrown
|
||||||
|
- svelte
|
||||||
|
- rails
|
||||||
|
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! 😊
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Mountain I Had to Climb
|
||||||
|
|
||||||
|
Early 2025: I opened my 19-year-old Rails codebase and knew this wouldn't be a migration: it would be a translation of an entire business. Quietly, I gave myself until June to prove it could work. If not, 2025 would be the final year for LocallyGrown.net.
|
||||||
|
|
||||||
|
I began working in secret. I didn't want to signal that the platform was in trouble and cause markets to jump ship early, but I also couldn't promise fixes to the mounting requests that had been piling up. If by June I didn't believe it would work, I'd begin winding down the system and helping markets migrate to competitors.
|
||||||
|
|
||||||
|
Everything rode on this attempt. I gave it long odds.
|
||||||
|
|
||||||
|
**The numbers were staggering:**
|
||||||
|
|
||||||
|
* **23 ActiveRecord models** with complex relationships
|
||||||
|
* **23 controllers** handling everything from user authentication to harvest coordination
|
||||||
|
* **Custom subdomain routing** isolating dozens of markets
|
||||||
|
* **Role-based permission system** with 5 user types across multiple contexts
|
||||||
|
* **Dynamic customization engine** allowing markets to inject custom HTML, CSS, and JavaScript
|
||||||
|
* **Complex pricing logic** with market-specific fees, grower percentages, and customer credits
|
||||||
|
* **Automated email workflows** triggered by state changes throughout the system
|
||||||
|
* **File upload handling** for product images, virtual farm tours, and market documents
|
||||||
|
* **Background job processing** for reports and notifications
|
||||||
|
|
||||||
|
Rails conventions interconnected each piece so tightly that removing Rails would collapse the system like a house of cards.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Challenges Quick Navigation
|
||||||
|
|
||||||
|
* [Challenge 1: Domain Model Translation](#challenge-1-the-domain-model-translation) - Untangling ActiveRecord into TypeScript services
|
||||||
|
* [Challenge 2: Schema Strategy](#challenge-2-schema-strategy-keep-vs-redesign) - Why I kept the messy Rails database
|
||||||
|
* [Challenge 3: Subdomain Multitenancy](#challenge-3-subdomain-multitenancy) - Building market isolation without Rails magic
|
||||||
|
* [Challenge 4: Permission System](#challenge-4-the-permission-system) - Centralizing scattered authorization checks
|
||||||
|
* [Challenge 5: Customization Redesign](#challenge-5-the-customization-system-redesign) - From dangerous injection to secure theming
|
||||||
|
* [Challenge 6: All-or-Nothing Migration](#challenge-6-the-all-or-nothing-migration-constraint) - Why parallel systems were impossible
|
||||||
|
* [Challenge 7: Asset Migration](#challenge-7-the-asset-migration-nobody-thinks-about) - Migrating 19 years of images to cloud storage
|
||||||
|
* [Challenge 8: Beta Testing Strategy](#challenge-8-the-beta-testing-strategy) - Two months of parallel preview testing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Strategy: Decompose, Translate, Rebuild
|
||||||
|
|
||||||
|
I couldn't just port Rails code to SvelteKit, as the paradigms are fundamentally different. Rails concentrated domain logic inside ActiveRecord; I moved that into framework-agnostic TypeScript services. SvelteKit handled routing/SSR, but business rules lived outside the web layer. I needed a systematic approach.
|
||||||
|
|
||||||
|
**My Migration Strategy:**
|
||||||
|
|
||||||
|
1. **Extract the domain model** from Rails into pure TypeScript classes
|
||||||
|
2. **Build a service layer** to handle business logic outside the framework
|
||||||
|
3. **Create database access patterns** that worked with Drizzle ORM instead of ActiveRecord
|
||||||
|
4. **Implement authentication and authorization** without Rails' built-in systems
|
||||||
|
5. **Preserve subdomain multitenancy** in a framework that doesn't natively support it
|
||||||
|
6. **Replace unsafe customization** with a secure theming system
|
||||||
|
7. **Maintain all existing URLs** to avoid breaking bookmarks and SEO
|
||||||
|
|
||||||
|
The key insight: I needed to extract what each piece of my Rails code was accomplishing, then rebuild that functionality using modern patterns.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Challenge 1: The Domain Model Translation
|
||||||
|
|
||||||
|
_Learn how to untangle business logic from ActiveRecord and rebuild it in clean TypeScript services._
|
||||||
|
|
||||||
|
Rails ActiveRecord encourages putting business logic directly in model classes. My `User` model alone was nearly 500 lines of mixed concerns: authentication, authorization, market permissions, and business rules all tangled together.
|
||||||
|
|
||||||
|
**Before (Rails):**
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
class User < ActiveRecord::Base
|
||||||
|
belongs_to :market
|
||||||
|
belongs_to :grower
|
||||||
|
has_many :orders
|
||||||
|
has_many :cart_items
|
||||||
|
has_many :managed_markets, class_name: 'Market', foreign_key: 'manager_id'
|
||||||
|
|
||||||
|
def full_name
|
||||||
|
"#{first_name} #{last_name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def can_manage_market?(market)
|
||||||
|
return true if superuser?
|
||||||
|
return true if market_manager? && managed_markets.include?(market)
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_active_cart?
|
||||||
|
cart_items.where(checked_out: false).any?
|
||||||
|
end
|
||||||
|
|
||||||
|
def total_purchases
|
||||||
|
orders.where(status: 'completed').sum(:total)
|
||||||
|
end
|
||||||
|
|
||||||
|
# ... 300+ more lines of mixed business logic
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
I had to untangle this into clean separation of concerns:
|
||||||
|
|
||||||
|
**After (TypeScript):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Pure domain model
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
marketId: string;
|
||||||
|
roles: UserRole[];
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Business logic in services
|
||||||
|
export class AuthorizationService {
|
||||||
|
canManageMarket(user: User, market: Market): boolean {
|
||||||
|
if (user.roles.includes('superuser')) return true;
|
||||||
|
if (user.roles.includes('manager') && user.marketId === market.id) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cart service
|
||||||
|
export class CartService {
|
||||||
|
async hasActiveCart(userId: string): Promise<boolean> {
|
||||||
|
const items = await db.select()
|
||||||
|
.from(cartItems)
|
||||||
|
.where(and(
|
||||||
|
eq(cartItems.userId, userId),
|
||||||
|
eq(cartItems.checkedOut, false)
|
||||||
|
));
|
||||||
|
return items.length > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Order service
|
||||||
|
export class OrderService {
|
||||||
|
async getTotalPurchases(userId: string): Promise<number> {
|
||||||
|
const result = await db.select({ total: sum(orders.total) })
|
||||||
|
.from(orders)
|
||||||
|
.where(and(
|
||||||
|
eq(orders.userId, userId),
|
||||||
|
eq(orders.status, 'completed')
|
||||||
|
));
|
||||||
|
return result[0]?.total || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This separation made testing easier and removed framework dependencies from business logic. But it meant manually recreating every Rails association and validation in TypeScript.
|
||||||
|
|
||||||
|
**Outcome:** Business rules left Rails' gravity and became testable, framework-agnostic services.
|
||||||
|
|
||||||
|
[↑ Back to navigation](#architecture-challenges-quick-navigation)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Challenge 2: Schema Strategy - Keep vs. Redesign
|
||||||
|
|
||||||
|
_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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
**The Temptation of a Fresh Start:**
|
||||||
|
|
||||||
|
I originally built the schema around ActiveRecord conventions from 2006:
|
||||||
|
* Complex many-to-many relationships
|
||||||
|
* A single user could be a customer, belong to one grower profile, and be a market manager
|
||||||
|
* Multiple users could share management of a single grower profile (family farms collaborating)
|
||||||
|
* Denormalized data for Rails performance optimizations that modern databases don't need
|
||||||
|
* I had left legacy columns in place from features I removed or refined years ago
|
||||||
|
* Naming conventions that made sense in Rails but felt awkward in TypeScript
|
||||||
|
|
||||||
|
I could have designed beautiful, normalized tables with proper constraints, better indexing, and naming that matched the modern domain model.
|
||||||
|
|
||||||
|
**Why I Kept the Old Schema:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- The existing schema reflected complex real-world relationships
|
||||||
|
CREATE TABLE products (
|
||||||
|
id int PRIMARY KEY,
|
||||||
|
grower_id int, -- References growers table, not users
|
||||||
|
market_id int,
|
||||||
|
name varchar(255),
|
||||||
|
price decimal(10,2),
|
||||||
|
quantity_available int,
|
||||||
|
created_at datetime, -- Rails timestamps
|
||||||
|
updated_at datetime
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE growers (
|
||||||
|
id int PRIMARY KEY,
|
||||||
|
market_id int,
|
||||||
|
name varchar(255),
|
||||||
|
-- Multiple users can manage this grower profile
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE users (
|
||||||
|
id int PRIMARY KEY,
|
||||||
|
email varchar(255),
|
||||||
|
grower_id int, -- Can belong to one grower profile (nullable)
|
||||||
|
market_id int,
|
||||||
|
admin tinyint,
|
||||||
|
volunteer tinyint,
|
||||||
|
-- A user can be customer, grower, manager, or all three
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Drizzle had to respect these complex relationships
|
||||||
|
export const products = mysqlTable('products', {
|
||||||
|
id: int('id').primaryKey(),
|
||||||
|
growerId: int('grower_id'), // NOT a user_id - references growers table
|
||||||
|
marketId: int('market_id'),
|
||||||
|
name: varchar('name', { length: 255 }),
|
||||||
|
price: decimal('price', { precision: 10, scale: 2 }),
|
||||||
|
quantityAvailable: int('quantity_available'),
|
||||||
|
createdAt: datetime('created_at'),
|
||||||
|
updatedAt: datetime('updated_at')
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**The Killer Advantages:**
|
||||||
|
|
||||||
|
1. **Zero Data Transformation:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Migration became trivially simple
|
||||||
|
mysqldump rails_production > backup.sql
|
||||||
|
mysql svelte_production < backup.sql
|
||||||
|
# Done. No ETL scripts, no data mapping, no conversion errors
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Development with Real Data:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// I could copy production data to dev and immediately test
|
||||||
|
// No mock data, no fixtures, actual market complexity
|
||||||
|
const realOrders = await db.select().from(orders).where(/* real conditions */);
|
||||||
|
// Found edge cases immediately that mock data would never reveal
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Instant Rollback Capability (in theory):**
|
||||||
|
If something went catastrophically wrong in the first hours, I could theoretically point Rails back at the database. The schema hadn't changed, just the MySQL version. (Though as we'll see in Challenge 6, version incompatibility made this impossible in practice.)
|
||||||
|
|
||||||
|
4. **Reduced Migration Risk:**
|
||||||
|
Every data transformation is a chance for corruption. By keeping the schema identical, I eliminated an entire category of potential failures. When you're doing an all-or-nothing migration at 2 AM, fewer moving parts is better.
|
||||||
|
|
||||||
|
**Living with the Compromises:**
|
||||||
|
|
||||||
|
Yes, it meant accepting some awkwardness:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Had to preserve Rails-era naming conventions
|
||||||
|
// Even when they didn't match modern naming standards
|
||||||
|
class GrowerService {
|
||||||
|
async getUsersForGrower(growerId: string) {
|
||||||
|
// One grower can have multiple users (family farm)
|
||||||
|
return db.select()
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.growerId, growerId));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGrowerForUser(userId: string) {
|
||||||
|
// But each user can only belong to one grower
|
||||||
|
const user = await db.select()
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.id, userId))
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!user?.growerId) return null;
|
||||||
|
|
||||||
|
return db.select()
|
||||||
|
.from(growers)
|
||||||
|
.where(eq(growers.id, user.growerId))
|
||||||
|
.first();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The role system complexity from Rails carried over
|
||||||
|
interface UserContext {
|
||||||
|
user: User;
|
||||||
|
roles: ('customer' | 'grower' | 'manager' | 'superuser')[];
|
||||||
|
growerProfile?: Grower; // Can belong to ONE grower profile
|
||||||
|
marketPermissions: MarketPermission[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
But these minor inconveniences were worth it for the ability to develop against real production data. Every day during development, I could sync the latest data and test with actual market orders, real product catalogs, and genuine edge cases that 19 years of production use had created.
|
||||||
|
|
||||||
|
My choice to keep the messy schema traded elegance for certainty — and certainty wins at 2 AM. If I was successful, I could always migrate to a cleaner schema later using all the tools Drizzle provides.
|
||||||
|
|
||||||
|
**The Validation:**
|
||||||
|
|
||||||
|
This decision was validated immediately. Being able to test with real production data meant I could verify that complex market workflows, grower relationships, and order patterns all worked correctly from day one. No mock data, no fixtures, just real-world complexity that ensured the new system would handle everything the old one did.
|
||||||
|
|
||||||
|
**Outcome:** Zero data transformation meant zero data corruption and the messy schema became my safety net.
|
||||||
|
|
||||||
|
[↑ Back to navigation](#architecture-challenges-quick-navigation)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Challenge 3: Subdomain Multitenancy
|
||||||
|
|
||||||
|
_Learn how to implement market isolation when the framework doesn't support it natively._
|
||||||
|
|
||||||
|
Rails made subdomain isolation look easy with a simple before_action filter:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
def get_market
|
||||||
|
subdomain = request.subdomain
|
||||||
|
@market = Market.find_by(subdomain: subdomain)
|
||||||
|
redirect_to_root unless @market
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
SvelteKit doesn't have built-in subdomain handling. I had to implement this at the infrastructure level:
|
||||||
|
|
||||||
|
**SvelteKit Solution:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// hooks.server.ts
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import { MarketService } from '$lib/server/services/MarketService';
|
||||||
|
|
||||||
|
export async function handle({ event, resolve }) {
|
||||||
|
// Extract subdomain from host
|
||||||
|
const host = event.request.headers.get('host') || '';
|
||||||
|
const subdomain = host.split('.')[0];
|
||||||
|
|
||||||
|
// Load market context
|
||||||
|
const market = await MarketService.findBySubdomain(subdomain);
|
||||||
|
if (!market) {
|
||||||
|
throw redirect(302, 'https://locallygrown.net/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add market to request context
|
||||||
|
event.locals.market = market;
|
||||||
|
|
||||||
|
return resolve(event);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Every page load now automatically has market context, just like Rails. But I had to manually ensure this worked with:
|
||||||
|
|
||||||
|
* Static site generation
|
||||||
|
* Server-side rendering
|
||||||
|
* Client-side navigation
|
||||||
|
* Development vs production environments (using `athens.locallygrown.lcl.host:5173` for local subdomain testing)
|
||||||
|
|
||||||
|
**Outcome:** Every market now loads with proper isolation, just like Rails but without the Rails magic.
|
||||||
|
|
||||||
|
[↑ Back to navigation](#architecture-challenges-quick-navigation)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Challenge 4: The Permission System
|
||||||
|
|
||||||
|
_See how to centralize authorization checks scattered throughout controllers and views._
|
||||||
|
|
||||||
|
Rails had a complex role-based system where users could have different permissions in different contexts:
|
||||||
|
|
||||||
|
* **Superusers:** Access to everything across all markets
|
||||||
|
* **Market Managers:** Full access within their assigned market
|
||||||
|
* **Growers:** Can manage their own products and orders
|
||||||
|
* **Customers:** Can browse and purchase
|
||||||
|
* **Volunteers:** Limited administrative functions
|
||||||
|
|
||||||
|
The Rails code scattered these checks throughout controllers and views:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# In controllers
|
||||||
|
before_filter :require_superuser_or_market_manager
|
||||||
|
before_filter :require_ownership, only: [:edit, :update, :destroy]
|
||||||
|
|
||||||
|
# In views
|
||||||
|
<% if can_edit_product?(@product) %>
|
||||||
|
<%= link_to "Edit", edit_product_path(@product) %>
|
||||||
|
<% end %>
|
||||||
|
```
|
||||||
|
|
||||||
|
I centralized this into a comprehensive authorization service:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export class AuthorizationService {
|
||||||
|
private user: User;
|
||||||
|
private market: Market;
|
||||||
|
|
||||||
|
constructor(user: User, market: Market) {
|
||||||
|
this.user = user;
|
||||||
|
this.market = market;
|
||||||
|
}
|
||||||
|
|
||||||
|
canEditProduct(product: Product): boolean {
|
||||||
|
// Superusers can edit anything
|
||||||
|
if (this.user.roles.includes('superuser')) return true;
|
||||||
|
|
||||||
|
// Market managers can edit products in their market
|
||||||
|
if (this.user.roles.includes('manager') && this.user.marketId === this.market.id) return true;
|
||||||
|
|
||||||
|
// Growers can edit their own products
|
||||||
|
if (this.user.roles.includes('grower') && product.growerId === this.user.growerId) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
canManageOrders(): boolean {
|
||||||
|
return this.hasRole(['superuser', 'manager']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private hasRole(roles: UserRole[]): boolean {
|
||||||
|
return roles.some(role => this.user.roles.includes(role));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then in SvelteKit pages, I computed permissions server-side and passed them to components:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// +page.server.ts
|
||||||
|
export async function load({ locals }) {
|
||||||
|
const auth = new AuthorizationService(locals.user, locals.market);
|
||||||
|
const product = await getProduct(params.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
product,
|
||||||
|
canEdit: auth.canEditProduct(product),
|
||||||
|
canManageOrders: auth.canManageOrders()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<!-- +page.svelte -->
|
||||||
|
<script lang="ts">
|
||||||
|
export let data;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if data.canEdit}
|
||||||
|
<a href="/products/{data.product.id}/edit">Edit Product</a>
|
||||||
|
{/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.
|
||||||
|
|
||||||
|
**Outcome:** Authorization logic moved from scattered checks to a single source of truth.
|
||||||
|
|
||||||
|
[↑ Back to navigation](#architecture-challenges-quick-navigation)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Challenge 5: The Customization System Redesign
|
||||||
|
|
||||||
|
_Understand the journey from dangerous HTML/CSS/JS injection to secure theming._
|
||||||
|
|
||||||
|
The old system let markets inject arbitrary HTML, CSS, and JavaScript which is powerful but dangerous:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# This was scary but effective
|
||||||
|
def custom_header
|
||||||
|
raw(@market.custom_header_html)
|
||||||
|
end
|
||||||
|
|
||||||
|
def custom_styles
|
||||||
|
content_tag(:style, @market.custom_css, type: 'text/css')
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
I needed something that preserved flexibility while eliminating security risks and enabling mobile optimization. My solution: a constrained theming system.
|
||||||
|
|
||||||
|
**Secure Theme System:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface ThemeConfig {
|
||||||
|
colors: {
|
||||||
|
primary: string;
|
||||||
|
secondary: string;
|
||||||
|
accent: string;
|
||||||
|
};
|
||||||
|
fonts: {
|
||||||
|
heading: string;
|
||||||
|
body: string;
|
||||||
|
};
|
||||||
|
layout: {
|
||||||
|
headerStyle: 'minimal' | 'full' | 'banner';
|
||||||
|
showMarketInfo: boolean;
|
||||||
|
featuredProductsCount: number;
|
||||||
|
};
|
||||||
|
customContent: {
|
||||||
|
welcomeMessage: string; // Sanitized Markdown
|
||||||
|
aboutSection: string; // HTML whitelist only
|
||||||
|
footerText: string; // No script injection
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Markets could customize colors, fonts, layout options, and content blocks, but couldn't inject arbitrary code. Content fields are sanitized with a strict HTML whitelist, preventing any script injection. The theme system generated CSS custom properties:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<!-- +layout.svelte -->
|
||||||
|
<style>
|
||||||
|
:global(:root) {
|
||||||
|
--color-primary: {theme.colors.primary};
|
||||||
|
--color-secondary: {theme.colors.secondary};
|
||||||
|
--font-heading: {theme.fonts.heading};
|
||||||
|
--font-body: {theme.fonts.body};
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
This preserved the custom look each market wanted while ensuring mobile responsiveness and security.
|
||||||
|
|
||||||
|
**Outcome:** Markets kept their unique identity without the security nightmares.
|
||||||
|
|
||||||
|
[↑ Back to navigation](#architecture-challenges-quick-navigation)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Challenge 6: The All-or-Nothing Migration Constraint
|
||||||
|
|
||||||
|
_Understand why shared databases and cross-market features made parallel systems impossible._
|
||||||
|
|
||||||
|
Unlike many migrations where you can run old and new systems in parallel, I faced a critical constraint: LocallyGrown.net uses a single shared database across all markets, with features that allow growers to sell across multiple markets by linking inventory. This meant I couldn't have some users on Rails while others used SvelteKit. It had to be everyone, all at once.
|
||||||
|
|
||||||
|
### Why Parallel Systems Were Impossible
|
||||||
|
|
||||||
|
The shared database architecture that made LocallyGrown powerful also made gradual migration impossible:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Single database, all markets share tables
|
||||||
|
-- Products can be linked across markets
|
||||||
|
-- Inventory is shared when growers sell at multiple markets
|
||||||
|
-- Orders from different markets all go in the same orders table
|
||||||
|
```
|
||||||
|
|
||||||
|
### Real-Time Conflict Risks
|
||||||
|
|
||||||
|
If Rails and SvelteKit were running simultaneously:
|
||||||
|
|
||||||
|
1. **Order Processing Chaos:**
|
||||||
|
* Customer starts order in Rails (market A still on old system)
|
||||||
|
* Adds products to cart, stored in database
|
||||||
|
* Market switches to SvelteKit mid-shopping
|
||||||
|
* Cart format incompatible, order lost or corrupted
|
||||||
|
|
||||||
|
2. **Inventory Nightmares:**
|
||||||
|
* Grower updates inventory in Rails interface
|
||||||
|
* Rails uses one caching strategy
|
||||||
|
* SvelteKit uses different caching
|
||||||
|
* Same product shows different quantities to different customers
|
||||||
|
* Overselling becomes inevitable
|
||||||
|
|
||||||
|
3. **Session Conflicts:**
|
||||||
|
* Rails sessions stored one way
|
||||||
|
* SvelteKit sessions stored differently
|
||||||
|
* User logged into Rails
|
||||||
|
* Hits SvelteKit page, appears logged out
|
||||||
|
* Tries to log in again, corrupts session
|
||||||
|
|
||||||
|
4. **Business Logic Discrepancies:**
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Rails: Calculates prices one way
|
||||||
|
def calculate_total
|
||||||
|
subtotal * (1 + market_fee) # Rounds at end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// SvelteKit: Slightly different calculation
|
||||||
|
function calculateTotal() {
|
||||||
|
return roundToCents(subtotal * (1 + marketFee)); // Rounds differently
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Same order could have different totals depending on which system processed it.
|
||||||
|
|
||||||
|
### The Shared Inventory Killer
|
||||||
|
|
||||||
|
The feature that lets growers sell at multiple markets made this worse:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Grower has 100 tomatoes
|
||||||
|
-- Sells at Athens, Madison, and Macon markets
|
||||||
|
-- Athens on Rails, Madison on SvelteKit
|
||||||
|
-- Both systems modifying the same inventory row
|
||||||
|
-- Race conditions everywhere
|
||||||
|
```
|
||||||
|
|
||||||
|
If Athens market (on Rails) and Madison market (on SvelteKit) both had customers ordering the same product simultaneously, the inventory updates would conflict, leading to overselling or lost sales.
|
||||||
|
|
||||||
|
### The Version Incompatibility Trap
|
||||||
|
|
||||||
|
Even if I wanted to try database replication, the technology stack made it impossible:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Rails 3 stack (circa 2011)
|
||||||
|
# MySQL 5.5
|
||||||
|
# mysql2 gem version 0.3.x
|
||||||
|
# ActiveRecord 3.x
|
||||||
|
|
||||||
|
# These old libraries literally couldn't connect to modern MySQL
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// SvelteKit stack (2025)
|
||||||
|
// MySQL 8.0
|
||||||
|
// Drizzle ORM
|
||||||
|
// Modern connection pooling
|
||||||
|
|
||||||
|
// Using authentication methods Rails 3 didn't understand
|
||||||
|
```
|
||||||
|
|
||||||
|
The Rails app was using MySQL 5.5 with authentication methods from 2011. The new stack required MySQL 8.0 with modern security features. I couldn’t even establish a connection with the old Ruby mysql2 gem to the new database server.
|
||||||
|
|
||||||
|
And trying to maintain real-time bidirectional replication between MySQL 5.5 and MySQL 8.0? That's not a migration strategy, that's a recipe for data corruption. The replication protocols, character encodings, and even timestamp handling had changed significantly between versions.
|
||||||
|
|
||||||
|
### The Legacy Layout Illusion
|
||||||
|
|
||||||
|
Since I couldn't run both systems, I tried to make the new system feel like the old one:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<!-- The legacy layout that looked like Rails -->
|
||||||
|
{#if user.preferredLayout === 'legacy'}
|
||||||
|
<div class="rails-style-layout">
|
||||||
|
<table class="old-school-product-table">
|
||||||
|
<!-- Mimicking 2010-era Rails HTML -->
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<ModernProductGrid />
|
||||||
|
{/if}
|
||||||
|
```
|
||||||
|
|
||||||
|
But this was just UI deep: underneath, everything was different:
|
||||||
|
* Different database queries
|
||||||
|
* Different calculation methods
|
||||||
|
* Different session handling
|
||||||
|
* Different URL structures
|
||||||
|
|
||||||
|
### The Point of No Return
|
||||||
|
|
||||||
|
The version gap created the most terrifying aspect: **no rollback was possible**.
|
||||||
|
|
||||||
|
Once I migrated the database from MySQL 5.5 to MySQL 8.0, the Rails app could never connect to it again. The authentication methods, character encodings, timestamp column types, and even the way NULL values were handled had all changed. It was a one-way door.
|
||||||
|
|
||||||
|
Once MySQL 8 went live, Rails 3 could never connect again. Once the new system was live and creating data, there was no rollback. There was only forward progress.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# The migration sequence that couldn't be undone:
|
||||||
|
1. Export data from MySQL 5.5
|
||||||
|
2. Transform data for MySQL 8.0 compatibility (mainly Textile markup to Markdown for all custom content)
|
||||||
|
3. Import into MySQL 8.0
|
||||||
|
4. Update DNS to SvelteKit
|
||||||
|
5. Hold your breath
|
||||||
|
|
||||||
|
# If anything went wrong after step 3, there was no going back
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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):
|
||||||
|
|
||||||
|
1. **2:00 AM:** Put Rails in maintenance mode
|
||||||
|
2. **2:05 AM:** Final backup of MySQL 5.5 database
|
||||||
|
3. **2:10 AM:** Run migration scripts to MySQL 8.0 (45 minutes of pure anxiety)
|
||||||
|
4. **2:55 AM:** Verify data integrity (spot checking critical tables)
|
||||||
|
5. **3:00 AM:** Update DNS to point to SvelteKit
|
||||||
|
6. **3:05 AM:** Remove maintenance mode
|
||||||
|
7. **5:00 AM:** Start breathing again
|
||||||
|
|
||||||
|
After two months of preview testing, when August 14 arrived, every market, every grower, every customer switched simultaneously.
|
||||||
|
|
||||||
|
If something went catastrophically wrong, my only option would be to restore the old MySQL 5.5 database and lose all transactions that happened after the migration. Every minute the new system ran made rolling back more painful.
|
||||||
|
|
||||||
|
This constraint shaped everything: I had to design the UI familiar enough that people wouldn’t panic, the data migration had to be perfect, and I had to fix issues in production immediately because there was no "switch back to the old system" option. The new system had to work, period.
|
||||||
|
|
||||||
|
**Outcome:** The one-way door forced perfection: there was no safety net, only forward momentum.
|
||||||
|
|
||||||
|
[↑ Back to navigation](#architecture-challenges-quick-navigation)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Challenge 7: The Asset Migration Nobody Thinks About
|
||||||
|
|
||||||
|
_Learn why migrating thousands of images and documents requires weeks of preparation._
|
||||||
|
|
||||||
|
One challenge that's easy to overlook: LocallyGrown had 19 years of uploaded assets: product photos, virtual farm tours, market documents. Rails stored everything in the filesystem. Not only did every URL need to keep working, but the new system required different image sizes for modern responsive galleries.
|
||||||
|
|
||||||
|
**The Hidden Complexity:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Rails stored everything on disk
|
||||||
|
/public/system/products/images/000/001/234/original/tomatoes.jpg
|
||||||
|
/public/system/products/images/000/001/234/thumb/tomatoes.jpg
|
||||||
|
/public/system/markets/documents/000/000/042/grower_agreement.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
The new system needed:
|
||||||
|
* S3-compatible cloud storage for scalability
|
||||||
|
* Multiple image sizes for responsive design
|
||||||
|
* WebP versions for modern browsers
|
||||||
|
* Preserved URLs for SEO and bookmarks
|
||||||
|
|
||||||
|
**The Migration Strategy:**
|
||||||
|
|
||||||
|
Luckily, I had kept all original images throughout the years, a decision that paid off massively. I could regenerate any size needed.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Node script for image processing
|
||||||
|
async function migrateProductImages() {
|
||||||
|
const sizes = {
|
||||||
|
thumb: { width: 150, height: 150 },
|
||||||
|
small: { width: 300, height: 300 },
|
||||||
|
medium: { width: 600, height: 600 },
|
||||||
|
large: { width: 1200, height: 1200 },
|
||||||
|
webp_thumb: { width: 150, height: 150, format: 'webp' },
|
||||||
|
webp_medium: { width: 600, height: 600, format: 'webp' }
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const product of products) {
|
||||||
|
const original = await getOriginalImage(product.id);
|
||||||
|
for (const [name, config] of Object.entries(sizes)) {
|
||||||
|
await sharp(original)
|
||||||
|
.resize(config.width, config.height, { fit: 'inside' })
|
||||||
|
.toFormat(config.format || 'jpeg')
|
||||||
|
.toFile(`processed/${product.id}/${name}.${config.format || 'jpg'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**The Rehearsal Process:**
|
||||||
|
|
||||||
|
For weeks before the migration, I ran synchronized uploads:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Nightly sync scripts running for weeks
|
||||||
|
rsync -av /rails/public/system/ /staging/assets/
|
||||||
|
node process-images.js
|
||||||
|
s3sync processed/ s3://locallygrown-assets/ --parallel=10
|
||||||
|
|
||||||
|
# URL rewrite rules to maintain compatibility
|
||||||
|
/system/products/images/* → https://assets.locallygrown.net/products/*
|
||||||
|
```
|
||||||
|
|
||||||
|
By migration night, all assets were already in place. When DNS switched over, every image URL continued working, but now served from cloud storage with modern responsive variants.
|
||||||
|
|
||||||
|
**Outcome:** 19 years of images migrated seamlessly. Users never knew their photos moved to the cloud.
|
||||||
|
|
||||||
|
[↑ Back to navigation](#architecture-challenges-quick-navigation)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Challenge 8: The Beta Testing Strategy
|
||||||
|
|
||||||
|
_Learn how to de-risk an irreversible migration with parallel preview testing._
|
||||||
|
|
||||||
|
Because a dual-run wasn't possible (see Challenge 6), I created a full preview environment at locallygrown.us that gave us two months to prepare with real users.
|
||||||
|
|
||||||
|
**The Preview System Architecture:**
|
||||||
|
|
||||||
|
Starting in June 2025, I launched a fully functional preview that mirrored production data:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Regular sync process
|
||||||
|
1. Export production data from Rails/MySQL 5.5
|
||||||
|
2. Sanitize sensitive data (emails replaced to prevent accidental sends)
|
||||||
|
3. Import into preview MySQL 8.0 database
|
||||||
|
4. Managers could test everything except real payments
|
||||||
|
|
||||||
|
// Access required two-step login:
|
||||||
|
1. Basic http auth credentials to keep people who stumbled upon the site out
|
||||||
|
2. Then their regular LocallyGrown credentials (the same ones they use on their live site)
|
||||||
|
```
|
||||||
|
|
||||||
|
**The Communication Campaign:**
|
||||||
|
|
||||||
|
**June 2025: The Announcement**
|
||||||
|
By June, I had proven to myself that the migration could work. The secret experiment had succeeded. Now came the nerve-wracking part: going public with a promise I had to keep.
|
||||||
|
|
||||||
|
I sent a comprehensive letter to all market managers explaining:
|
||||||
|
* Why we needed to migrate (infrastructure costs, mobile support, integration limitations)
|
||||||
|
* What would change and what would stay the same
|
||||||
|
* How to access their preview at [market].locallygrown.us
|
||||||
|
* Promise of one month's notice before the actual migration
|
||||||
|
|
||||||
|
**July 2025: Building Confidence**
|
||||||
|
Regular updates (Migration Updates #1-4) with:
|
||||||
|
* Progress reports on feature completion
|
||||||
|
* Instructions for testing specific workflows
|
||||||
|
* Screenshots of the new design system
|
||||||
|
* Reassurances about data preservation
|
||||||
|
|
||||||
|
**July 31: Go Live Date Confirmed**
|
||||||
|
* Confirmed August 14 migration date
|
||||||
|
* Outlined the exact timeline (2-3 AM Eastern)
|
||||||
|
* Gave managers a week to finalize customizations
|
||||||
|
* "If anything goes unexpectedly, I will keep the existing site live"
|
||||||
|
|
||||||
|
**August 7: Final Week**
|
||||||
|
* Stopped resetting preview customizations
|
||||||
|
* Managers could configure their preferred layouts
|
||||||
|
* Last chance for feedback and concerns
|
||||||
|
* Daily communication about readiness
|
||||||
|
|
||||||
|
**The Communication Infrastructure:**
|
||||||
|
|
||||||
|
I built features specifically for the migration:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<!-- In-app announcements system -->
|
||||||
|
<AnnouncementBanner>
|
||||||
|
<h3>System Migration: August 14</h3>
|
||||||
|
<p>The new system goes live in {daysRemaining} days.</p>
|
||||||
|
<a href="/migration-guide">See what's changing →</a>
|
||||||
|
</AnnouncementBanner>
|
||||||
|
```
|
||||||
|
|
||||||
|
**The Tutorial System:**
|
||||||
|
|
||||||
|
Knowing the interface was different, I created interactive tutorials:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Market shopping tutorial
|
||||||
|
const shoppingSteps = [
|
||||||
|
{ element: '.category-filter', text: 'Filter products by category here' },
|
||||||
|
{ element: '.search-box', text: 'Search for specific products' },
|
||||||
|
{ element: '.add-to-cart', text: 'Click to add items to your cart' },
|
||||||
|
{ element: '.cart-icon', text: 'View your cart here' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Grower inventory tutorial
|
||||||
|
const inventorySteps = [
|
||||||
|
{ element: '.quick-edit', text: 'Update quantities inline' },
|
||||||
|
{ element: '.bulk-actions', text: 'Select multiple products' },
|
||||||
|
{ element: '.save-all', text: 'Save all changes at once' }
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Features Built for Migration:**
|
||||||
|
|
||||||
|
1. **Interface Style Preferences**
|
||||||
|
* Markets could choose which user groups saw legacy vs modern interface
|
||||||
|
* Allowed gradual adoption within each market
|
||||||
|
* "Keep customers on the familiar legacy interface while testing the modern interface with your staff"
|
||||||
|
|
||||||
|
2. **Comprehensive Documentation**
|
||||||
|
* New docs site with guides for managers, growers, and customers
|
||||||
|
* "What's New" guide to share with users
|
||||||
|
* Technical documentation for future developers
|
||||||
|
|
||||||
|
3. **Design System Showcase**
|
||||||
|
* Live preview at locallygrown.net/showcase
|
||||||
|
* Demonstrated the mobile-first approach
|
||||||
|
* Built confidence in the new design
|
||||||
|
|
||||||
|
**The Feedback and Refinement:**
|
||||||
|
|
||||||
|
The preview period was crucial for discovering issues:
|
||||||
|
|
||||||
|
* **Customization preservation:** "95%+ feature complete"
|
||||||
|
* **Mobile-first admin screens:** Complete redesign for managers
|
||||||
|
* **Legacy layout option:** Keeping the familiar interface available
|
||||||
|
* **Theme builder:** Started building custom theme capabilities
|
||||||
|
|
||||||
|
**What the Preview Surfaced:**
|
||||||
|
The preview period helped identify workflow issues, UI confusion points, and missing features that only became apparent with real users testing their actual day-to-day tasks.
|
||||||
|
|
||||||
|
**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."
|
||||||
|
|
||||||
|
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."
|
||||||
|
|
||||||
|
The key message throughout: "Your data is safe, and I'm taking every precaution."
|
||||||
|
|
||||||
|
**Why This Mattered:**
|
||||||
|
|
||||||
|
When launch day came, it wasn't a surprise. Power users had been using the system for 8 weeks. Market managers had tested their specific workflows. Growers knew what to expect. The tutorials were refined based on real confusion points.
|
||||||
|
|
||||||
|
Yes, we still had issues (as Part 4 will detail), but imagine if we hadn't had this preview period. Instead of "the button moved" complaints, I would have had "nothing works" disasters.
|
||||||
|
|
||||||
|
Even with the preview system, some problems slipped through. The real production load, real money, and real urgency would reveal new issues. But, it transformed the migration from a blind leap to a calculated risk.
|
||||||
|
|
||||||
|
**Outcome:** The preview period gave me the muscle memory to handle post-launch fires. I had enough experience explaining the new system to identify and fix problems in real-time.
|
||||||
|
|
||||||
|
[↑ Back to navigation](#architecture-challenges-quick-navigation)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Development Process
|
||||||
|
|
||||||
|
Working evenings and weekends in secret meant I needed extreme discipline and clear priorities. No one knew I was attempting this rebuild: not the market managers, not the users, not even my closest colleagues. The weight of potentially shutting down LocallyGrown.net if I failed made every coding session feel critical.
|
||||||
|
|
||||||
|
**My Workflow:**
|
||||||
|
|
||||||
|
* **Monday-Wednesday:** Review my Rails implementation, plan TypeScript equivalents
|
||||||
|
* **Thursday-Friday:** Code new features, write tests
|
||||||
|
* **Saturday:** Integration work, bug fixes (I set a 10:30 PM "stop coding" alarm; I rarely obeyed it)
|
||||||
|
* **Sunday:** Deploy to staging, test with real data
|
||||||
|
|
||||||
|
Every commit had to be production-ready. I couldn't afford broken builds when I only had a few hours each day.
|
||||||
|
|
||||||
|
**Key Tools:** TypeScript, Drizzle ORM, Vitest, Docker, linear process (one feature at a time, no shortcuts)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What's Next: The Reality of Production
|
||||||
|
|
||||||
|
After six months of methodical development, I had built a complete SvelteKit application that replicated every Rails feature. The code was tested, the architecture was solid, and I was ready for the cutover described in Challenge 6.
|
||||||
|
|
||||||
|
Then real users started using it, and I discovered what I'd actually missed.
|
||||||
|
|
||||||
|
**Part 4 will cover:**
|
||||||
|
|
||||||
|
* The first 48 hours after the 2 AM cutover
|
||||||
|
* When "it works just like before" wasn't actually true
|
||||||
|
* Product filtering bugs that made shopping frustrating
|
||||||
|
* Growers struggling to manage inventory with the new interface
|
||||||
|
* Market managers overwhelmed supporting confused users
|
||||||
|
* Site customizations that didn't quite translate to the new system
|
||||||
|
* Payment calculation errors that affected real money
|
||||||
|
* How fixing "small" workflow issues became the real work
|
||||||
|
|
||||||
|
This taught me that migrating features, writing tests, and inviting testers isn't enough. And when you can't roll back, every issue becomes urgent.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_This is part three of a series documenting the rescue and modernization of LocallyGrown.net._
|
||||||
|
|
||||||
|
Part 4 is the messy part: real users, real money, and the bugs that only show up when a farm manager is closing orders at 11:58 PM.
|
||||||
|
|
||||||
|
### The Series
|
||||||
|
|
||||||
|
1. [From Accidental Discovery to Agricultural Infrastructure (2002-2011)](https://blog.kestrelsnest.social/posts/locallygrown-origin-story/)
|
||||||
|
2. [The 23-Year Rescue Mission: Saving Agricultural Innovation from Technical Extinction](https://blog.kestrelsnest.social/posts/locallygrown-rescue-mission/)
|
||||||
|
3. **The Architecture Challenge: Translating 19 Years of Rails Logic to Modern SvelteKit** ← _You are here_
|
||||||
|
4. [The Reality of Production: When Hope Meets Live Users](https://blog.kestrelsnest.social/posts/locallygrown-reality-of-production/)
|
||||||
|
5. [Lessons from the Solo Developer Using Modern Tools](https://blog.kestrelsnest.social/posts/locallygrown-lessons/)
|
||||||
|
6. The Future: Building on Modern Foundations
|
@@ -0,0 +1,341 @@
|
|||||||
|
---
|
||||||
|
title: "The Reality of Production: When Hope Meets Live Users"
|
||||||
|
description: Launching with 3,000 passing tests still led to two weeks of production bugs—fees, permissions, Stripe, email, and a thousand cuts. How I triaged 314 fixes, kept markets running, and what I’d change next time.
|
||||||
|
date: 2025-09-17T01:44:05.496Z
|
||||||
|
preview: ""
|
||||||
|
draft: false
|
||||||
|
tags:
|
||||||
|
- locallygrown
|
||||||
|
- svelte
|
||||||
|
- sveltekit
|
||||||
|
- rails
|
||||||
|
categories:
|
||||||
|
- locallygrown
|
||||||
|
lastmod: 2025-09-22T20:55:03.509Z
|
||||||
|
keywords:
|
||||||
|
- locallygrown
|
||||||
|
- svelte
|
||||||
|
- rails
|
||||||
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Launch Morning: The DNS Disaster
|
||||||
|
|
||||||
|
August 14, 2025, Thursday morning. What should have been a five-minute DNS switch became a multi-hour nightmare. Years ago, I had set up Cloudflare for the domain, a detail completely forgotten after years of not needing to touch DNS. The changes I made weren't propagating. The "coming soon" page stayed up while I diagnosed what was happening.
|
||||||
|
|
||||||
|
What should have been done by 3 AM dragged on past dawn. By the time I untangled the Cloudflare mess and got DNS propagating correctly, I was already exhausted. But finally, the new LocallyGrown.net was live.
|
||||||
|
|
||||||
|
I'd taken two days off from my regular job, planning to relax, handle a few tech support requests, maybe squash a bug or two. Mostly relax.
|
||||||
|
|
||||||
|
Instead, I got maybe eight hours of sleep total over the next four days.
|
||||||
|
|
||||||
|
The first manager email arrived within hours. Then another. Then another. By evening, my inbox was a cascade of increasingly urgent discoveries:
|
||||||
|
|
||||||
|
- Desktop views showing blank pages while mobile worked fine
|
||||||
|
- The three dollar flat fee disaster: every Stripe payment charging $3 instead of 3%
|
||||||
|
- Growers locked out of editing their own products
|
||||||
|
- Invoices no longer grouped by grower, breaking the physical workflow of how volunteers distributed food
|
||||||
|
- Some customers charged zero, others charged twice
|
||||||
|
- "Internal server error" when adding milk to cart
|
||||||
|
- Decimal amounts wouldn't save, so managers couldn't adjust balances for $12.50 worth of extras
|
||||||
|
|
||||||
|
One manager was getting calls, texts, and emails from confused customers. By evening of the first day, she had only 7 orders when she usually had dozens. Customers were placing duplicate orders thinking the first hadn't gone through. Growers couldn't reactivate products after vacation mode. The harvest summary page showed negative quantities (-1 egg for -$5).
|
||||||
|
|
||||||
|
But there was one bright spot: "The mobile checkout is so much better than before," one customer told their manager. At least something was working better.
|
||||||
|
|
||||||
|
The real testing had just begun.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug Category #1: The Stupid Math Mistakes
|
||||||
|
|
||||||
|
**The Most Embarrassing Bug:**
|
||||||
|
|
||||||
|
Market credit card fees weren't being calculated as percentages; they were being added as flat amounts.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// What I wrote (wrong):
|
||||||
|
const surcharge = market.customerPercentage; // If 3%, adds $3.00
|
||||||
|
|
||||||
|
// What it should have been:
|
||||||
|
const surcharge = subtotal * (market.customerPercentage / 100); // 3% of subtotal
|
||||||
|
```
|
||||||
|
|
||||||
|
**How This Slipped Through:**
|
||||||
|
|
||||||
|
My test data was too convenient:
|
||||||
|
|
||||||
|
- \$100 order with 3% fee = $103 total ✓
|
||||||
|
- Looked correct, but for entirely wrong reasons
|
||||||
|
- The three dollars was a flat fee, not 3% of $100
|
||||||
|
|
||||||
|
On real orders:
|
||||||
|
|
||||||
|
- \$47.50 order with 3% fee = \$50.50 (wrong - added $3)
|
||||||
|
- Should have been \$48.93 (3% = $1.43)
|
||||||
|
|
||||||
|
Markets were overcharging on small orders, undercharging on large ones.
|
||||||
|
|
||||||
|
My Git logs from that first week are littered with fixes for math bugs. Markets can customize their pricing structure in an almost infinite number of ways, and even with thousands of tests I didn't capture everything I needed to.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug Category #2: Role-Based Permission Failures
|
||||||
|
|
||||||
|
**What I Thoroughly Tested:** Manager accounts doing manager things.
|
||||||
|
|
||||||
|
**What I Didn't Test Enough:** Grower-only accounts, volunteer-only accounts, customer-with-no-special-role accounts.
|
||||||
|
|
||||||
|
The bugs were everywhere, all in the direction of being too restrictive:
|
||||||
|
|
||||||
|
- Growers couldn't edit their own products (Aug 15)
|
||||||
|
- Volunteers couldn't access the volunteer functions they needed (Aug 28)
|
||||||
|
- Growers couldn't remove items from harvest lists (Aug 22)
|
||||||
|
- Non-grower admins couldn't add products (Aug 17)
|
||||||
|
- Growers couldn't access their Sales History (Aug 27)
|
||||||
|
|
||||||
|
Root cause: I validated backend rules but missed UI visibility checks for non-manager roles.
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<!-- The broken check -->
|
||||||
|
{#if user.isAdmin || user.isManager}
|
||||||
|
<button>Edit Product</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Should have been -->
|
||||||
|
{#if user.isAdmin || user.isManager || (user.isGrower && product.growerId === user.growerId)}
|
||||||
|
<button>Edit Product</button>
|
||||||
|
{/if}
|
||||||
|
```
|
||||||
|
|
||||||
|
My manual testing had a fatal flaw: I tried to test every kind of account, but I mostly spent my time viewing the site as a manager would. Managers can do everything. I ran out of time before I could test every kind of account with every button and control in the system. The back end logic was correct and tested, but that doesn't help when the button you need to click isn't even visible.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug Category #3: Payment Processing Disasters
|
||||||
|
|
||||||
|
The Stripe integration broke in spectacular ways. The first disaster happened while we were still in the beta period, when I had created a whole set of safeguards that allowed for testing every aspect of the payment process but simulated the final step of charging the card. I'd accidentally left one path unguarded, and a manager charged several of her customers for copies of their real orders. It was painful, and we hadn't even launched yet.
|
||||||
|
|
||||||
|
LocallyGrown’s payment flow is unlike typical e-commerce. When customers place their orders, often times the produce is still in the field. It's not unusual for orders to be received five days after they're placed, and there are many, many reasons why what gets delivered doesn't exactly match what was ordered. The simplest way to handle this is to verify the card, save it as a payment method tied to the customer, and then charge that card after the order is picked up and everything is verified and reconciled.
|
||||||
|
|
||||||
|
Stripe allows me to do all this, though it's not their typical way of handling payments. On the old system, I was using an extremely early version of their APIs, but they have added a lot of complexity since then and that simplicity was not available to me. I tested everything I could, using the test environment they provide as well as running countless small real charges to my personal cards. It seemed as ready as it could ever be, but once the public started using the system, the bugs started appearing.
|
||||||
|
|
||||||
|
Every time one was reported, it became the highest priority. It was imperative that I get a fix in place before anyone else was affected, and with many markets running cards on different schedules, I had to act quickly. Luckily there weren't many, but when they happened I had to drop everything and fix them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug Category #4: Communication Breakdown
|
||||||
|
|
||||||
|
My application sends thousands of emails every day. Customers need to be kept informed of what to expect when they receive their order. Growers need to know what to harvest. Managers expect a constant flow of information delivered to their inbox. All of this information is available in real time on the website, but when you're riding a tractor or setting up canopies in a parking lot, sometimes pulling up an email is the best way to get the information you need.
|
||||||
|
|
||||||
|
I'd completely revamped the email system to be more flexible, to allow greater insight into deliverability, and to be friendlier in tone. It was a big change, and I was proud of it. But it also meant that I had to re-write all of the templates from scratch, include both plain text and formatted versions of each, and make sure they called the right variables in the right places.
|
||||||
|
|
||||||
|
And of course I missed a few things here and there that looked good to my eyes but were noticed immediately when used by the markets. In a few places I used the old Rails variable names, so a gap appeared in an email instead of, say, a product name. Or in the process of changing the layout of a nicely formatted email I omitted a column so the growers didn't have the contact information for a customer they were used to seeing.
|
||||||
|
|
||||||
|
None of these were major problems, and they were easy to fix. But, they piled up on top of everything else and became another thing for my customers to be frustrated with.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug Category #5: Death by a Thousand Cuts
|
||||||
|
|
||||||
|
The Git history tells the rest: CSV exports with wrong columns. iPhone photos that wouldn't upload. Infinite scroll that wasn't infinite. Password resets that didn't reset. Bots flooding the logs so I couldn't see real errors.
|
||||||
|
|
||||||
|
Every subsystem had issues. Every feature had edge cases. Every workflow had something I'd missed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Human Cost
|
||||||
|
|
||||||
|
Market managers were drowning. One manager sent me 8 detailed bug reports in the first week alone. She was simultaneously:
|
||||||
|
|
||||||
|
- Fielding calls from confused customers
|
||||||
|
- Helping growers who couldn't access their products
|
||||||
|
- Manually adjusting incorrect charges
|
||||||
|
- Trying to run a physical market with broken invoices
|
||||||
|
|
||||||
|
The invoice bug wasn't just about data display. When items weren't grouped by grower, volunteers had to walk back and forth across the market instead of going down the line. Physical workflow, broken by code.
|
||||||
|
|
||||||
|
The "can't use decimals" bug meant managers couldn't close out their markets. They literally couldn't mark orders complete because they couldn't enter $12.50 for extras, only $12 or $13.
|
||||||
|
|
||||||
|
This wasn't just a website with bugs. This was dozens of live food systems, hundreds of growers, thousands of customers depending on me to keep their local food networks running. Real businesses. Real money. Real communities.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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"
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## My Personal Reality
|
||||||
|
|
||||||
|
For the most part, my customers (the market managers) were understanding. They could see I was responding quickly and decisively. A few were angry at the surprise instability, and might still be angry.
|
||||||
|
|
||||||
|
I was overwhelmed. My morale was at an all-time low. My disappointment in myself for not catching so many bugs that were obvious in hindsight was immense. Every bug report felt like a personal failure. Every "this used to work" comment cut deep.
|
||||||
|
|
||||||
|
I've been in the software industry long enough to see many, many rollouts go horribly wrong. Goodwill can be destroyed overnight, and some large companies never recover. As a consulting developer, I've even been called in to help fix the messes caused by these disasters. I had to keep reminding myself that I've seen much worse, and even if it didn't go as planned I was capable of fixing it.
|
||||||
|
|
||||||
|
And I was also determined to get it done. There was no rolling back, so I had to go forward. I had to dig my way out and redeem myself and the choices I made along the way.
|
||||||
|
|
||||||
|
What kept me going was knowing this wasn't about me. It was about the communities depending on this system.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Unsung Heroes
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
By Week 3, the emails changed tone. The bug reports became less frequent. The managers stopped apologizing for "all the emails."
|
||||||
|
|
||||||
|
Success wasn't about the code being perfect. It was about the system becoming invisible again: managers could run their markets without fighting the software.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lessons From The Bug Apocalypse
|
||||||
|
|
||||||
|
### 1. Test Data Can Hide the Truth
|
||||||
|
|
||||||
|
Using convenient test values (\$100 orders, 10% fees) hides calculation bugs. Real-world data is messy: $47.83 orders with 2.75% fees reveal rounding errors immediately. I had an extensive library of test data, but most of the products had nice round numbers that were easy to verify but hid the bugs.
|
||||||
|
|
||||||
|
Seed fixtures with non-round prices and odd percentages; add randomized totals to catch rounding errors.
|
||||||
|
|
||||||
|
### 2. Your Primary Account Blinds You
|
||||||
|
|
||||||
|
Testing mainly with your admin account means you'll miss most permission bugs. You need test accounts for every role combination, and you need to use them repeatedly, even when you just change a single button.
|
||||||
|
|
||||||
|
Automate smoke flows per role; assert on control visibility, not just API responses.
|
||||||
|
|
||||||
|
### 3. Mock APIs Aren't Reality
|
||||||
|
|
||||||
|
Stripe test mode worked perfectly with clean test cards. Production Stripe used with real cards and long-standing accounts worked differently. I needed to better understand the newer APIs and how they differed from the old simple ones I was using.
|
||||||
|
|
||||||
|
Mirror prod settings in a staging account; record/replay webhooks and handle retries/duplicate events.
|
||||||
|
|
||||||
|
### 4. The Correct Backend + Broken Frontend = Broken System
|
||||||
|
|
||||||
|
A single wrong Svelte condition can make perfect backend logic useless.
|
||||||
|
|
||||||
|
Add component tests for visibility conditions and null/“unlimited” states.
|
||||||
|
|
||||||
|
### 5. Email Templates Are Code
|
||||||
|
|
||||||
|
They need testing. They need error handling. They need to handle missing data gracefully. "Hello undefined" is not acceptable.
|
||||||
|
|
||||||
|
Snapshot test rendered templates with missing fields; fail builds on “undefined” placeholders.
|
||||||
|
|
||||||
|
### 6. The Frontend Testing Gap
|
||||||
|
|
||||||
|
My backend had 3000 tests. Rock solid. But Svelte 5's testing story in 2025? Still evolving.
|
||||||
|
|
||||||
|
@testing-library/svelte had experimental Svelte 5 support with known bugs. The `mount()` function threw "not available on the server" errors in jsdom. Testing `$derived` runes didn't detect reactivity changes. Cypress and Playwright worked for basic flows but couldn't properly test Svelte 5's new reactive primitives.
|
||||||
|
|
||||||
|
Ideally, I'd have automated tests running as manager, grower, volunteer, and customer accounts at various screen sizes. More critically, these would have caught the permission-based bugs where essential buttons and controls were hidden from the very users who needed them: growers who couldn't edit their own products, volunteers locked out of their tools. That's still the goal. But with the testing tools available and the Rails system dying, I had to choose: wait for the ecosystem to mature, or launch with manual testing only for the frontend.
|
||||||
|
|
||||||
|
I chose to launch.
|
||||||
|
|
||||||
|
Until tooling stabilizes, lean on Playwright smoke suites across roles and screen sizes; treat them as gate checks.
|
||||||
|
|
||||||
|
### 7. Edge Cases Aren't Edge Cases
|
||||||
|
|
||||||
|
Products with unlimited inventory, markets with 0% fees, users with multiple roles. These aren't edge cases. They're Tuesday. When I offer near-unlimited flexibility, I'm taking on the responsibility to keep the freedom I've given working.
|
||||||
|
|
||||||
|
Promote “edge” scenarios to first-class test data and UX checks.
|
||||||
|
|
||||||
|
### 8. Beta Testing Has Limits
|
||||||
|
|
||||||
|
The handful of managers who beta tested provided invaluable feedback. But they weren't a representative sample. I set up perfect copies of every market with real data, gave two months of access, sent regular updates. Still, most managers (including those with the most complex customizations) waited until launch day.
|
||||||
|
|
||||||
|
The lesson: Beta testing helps, but it's a self-selecting sample. The users with the most complex workflows are often too busy to test until they have no choice.
|
||||||
|
|
||||||
|
Identify high-risk markets and schedule guided sessions; don’t rely on opt-in.
|
||||||
|
|
||||||
|
### 9. Launch and Fix > Wait for Perfect
|
||||||
|
|
||||||
|
The old Rails system was dying. Every day I delayed was another day it might fail completely. Launching with bugs I could fix was better than not launching at all.
|
||||||
|
|
||||||
|
Pair fast rollback-like mitigations (feature flags, circuit breakers) with rapid patch cadence.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Where We Are Now
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
The migration isn't complete, but we're getting there. And more importantly: we're still here.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What's Next: How One Developer Used Every Tool Available
|
||||||
|
|
||||||
|
From day one of this migration, I used the current generation of programming tools to accomplish what would have been impossible alone. Enhanced IDE autocomplete, AI-powered code generation, groups of specialized agents I created and orchestrated. These weren't crutches; they were force multipliers.
|
||||||
|
|
||||||
|
But let's talk about that loaded term: "AI." Like "farming," it's a broad term that covers both the exploitative and the ethical. Most food comes from factory farms that destroy communities and environments. But LocallyGrown.net exists to support the farmers who grow food responsibly. Similarly, many AI companies are built on stolen art and exploited resources. But there are also thoughtfully-created development tools that learned from code developers explicitly shared to help others.
|
||||||
|
|
||||||
|
I was the architect, the decision maker, the one who read and understood every line of code before it was committed. But I typed with more than just my fingers, using tools that amplified rather than replaced my expertise.
|
||||||
|
|
||||||
|
**Part 5 will cover:**
|
||||||
|
|
||||||
|
- Why "AI" (like "farming") isn't a monolith, and why that distinction matters
|
||||||
|
- How modern development tools made a solo migration of 23 years of code possible
|
||||||
|
- The reality of being the human in the loop: reviewing, correcting, and owning every line
|
||||||
|
- The ethics of using AI tools while opposing AI exploitation
|
||||||
|
- Building a workflow that let me multiply my efforts without losing control
|
||||||
|
|
||||||
|
This is the story of using every available responsible tool to save a system that feeds communities. Not because I couldn't code, but because I could code smarter, and because sometimes the right tool for the job happens to use machine learning.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_This is part four of a series documenting the rescue and modernization of LocallyGrown.net._
|
||||||
|
|
||||||
|
### The Series
|
||||||
|
|
||||||
|
1. [From Accidental Discovery to Agricultural Infrastructure (2002-2011)](https://blog.kestrelsnest.social/posts/locallygrown-origin-story/)
|
||||||
|
2. [The 23-Year Rescue Mission: Saving Agricultural Innovation from Technical Extinction](https://blog.kestrelsnest.social/posts/locallygrown-rescue-mission/)
|
||||||
|
3. [The Architecture Challenge: Translating 19 Years of Rails Logic to Modern SvelteKit](https://blog.kestrelsnest.social/posts/locallygrown-architecture-challenge/)
|
||||||
|
4. **The Reality of Production: When Hope Meets Live Users** ← _You are here_
|
||||||
|
5. [Lessons from the Solo Developer Using Modern Tools](https://blog.kestrelsnest.social/posts/locallygrown-lessons/)
|
||||||
|
6. The Future: Building on Modern Foundations
|
1159
content/posts/2025-09-22-lessons-from-the-solo-developer-trenches.md
Normal file
1159
content/posts/2025-09-22-lessons-from-the-solo-developer-trenches.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -58,7 +58,7 @@
|
|||||||
</nav>
|
</nav>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- with .Site.Menus.tertiary }}
|
{{- with .Site.Menus.tertiary }}
|
||||||
<br /><strong>Auxillary Pages</strong>
|
<br /><strong>Auxiliary Pages</strong>
|
||||||
<nav class="app-header-menu">
|
<nav class="app-header-menu">
|
||||||
{{- range $key, $item := . }}
|
{{- range $key, $item := . }}
|
||||||
{{- if ne $key 0 }}
|
{{- if ne $key 0 }}
|
||||||
|
BIN
static/default-og.png
Normal file
BIN
static/default-og.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 365 KiB |
Reference in New Issue
Block a user