A real FileMaker-to-web migration for a travel agency. Two developers, 528 commits in 19 days, 14 feature modules, 5 architecture reviews. Testing and data migration still ahead.
The Travel Agency Migration
A travel agency runs custom group trips. Family programs, student experiences, community delegations. Full-service operation: flights, hotels, tours, speakers, guides, transport, catering. Twenty-five years in business, fifty-thousand travelers. And a FileMaker database that grew over decades, layered and patched until no one could quite say how all the pieces connected.
The developer got the call on April 27. A form submission, an email exchange, a DDR sent through Google Drive, a video walkthrough of the application. Standard FileMaker migration intake. By April 28, the repo had its first commit.
Discovery: what are we actually building
The FileMaker database was a DDR document, hundreds of pages of schema definitions. Tables, fields, relationships, scripts, layouts. The developer's standard approach: pull the DDR, transcribe the video walkthrough, cross-reference every table against every layout to build a complete picture. The client answered questions in batches. Screenshots came in waves. A company research document mapped the business: two international offices, a team of fifteen, repeat clients spanning decades.
The first week was all analysis. Schema design: splitting a monolithic FileMaker structure into domain files, eleven of them. API design: ten domain files aligned with the schema. Migration plan: eight phases from foundation through budget tracking, bookers, bookkeeping, cash flow, post-trip reconciliation, and canned templates. All four phases run through the developer's FileMaker migration toolkit — a DDR parser that extracts structured specs from the XML export, a discovery engine that surfaces requirements, a tech stack recommendation matrix, and a plan generator that produces SQL schema and API designs. The toolkit is open-source: github.com/solutionscay/migrate-filemaker.
The stack decision landed on Cloudflare: D1 for the database, Hono for the API workers, Vue 3 with PrimeVue for the frontend, Vite for the build. No servers. No containers. Code ships to the edge.
By April 30, the foundation was committed. Auth with JWT and role switching. App shell with sidebar navigation. Design tokens in Tailwind and PrimeVue. The migration plan ran to 586 lines of markdown covering every table, every route, every UI screen.
Foundation and velocity
The developer built the foundation alone. April 28 through May 6: auth with JWT and role switching, the app shell, design tokens, the trip list, the itinerary grid, activity CRUD. Schema design, API architecture, migration plan. Nine straight days of commits before the client wrote a single line of code. The client's commits on April 29 through May 1 were QA context and requirements: answering discovery questions, providing screenshots, clarifying business rules. No development yet.
The client started developing May 7. The velocity difference was immediate. The developer's commits stayed steady: 22, 12, 17, 24, 16 per day. The client's ramp was exponential: 3 on May 7, then 29, 4, 12, 60, 73, 105. Peak day was May 13: 105 client commits. Total across both authors: 528 commits in 19 days.
COMMITS BY DAY
Jose Sam
Apr 28 3 —
Apr 29 7 10
Apr 30 10 11
May 1 19 6
May 2 3 —
May 3 7 —
May 4 9 —
May 5 2 —
May 6 11 —
May 7 22 3
May 8 12 29
May 9 — 4
May 10 17 12
May 11 24 60
May 12 16 73
May 13 12 105
May 14 — 17
May 15 9 11
May 16 4 —
─────────────────
Total 187 341 (528)
Two different development styles, visible in the commit shapes. The developer's commits were architectural: schema files, route structures, design tokens, migration plans, architecture review prompts. The client's commits were features, built fast on the scaffold the developer provided.
The core went in first: trip management, the itinerary grid, activity editing. This is the screen every user sees every day, a DataTable of trip activities with date sorting, category filtering, status tracking. Budget calculations followed: per-person cost at fixed tiers, shekel rate conversion, quote versus actual comparisons, revision history snapshots.
Then the surrounding modules in rapid succession, mostly from the client. Contacts, a full CRM with skills, classes, and hotel assignments. Bookkeepers, tracking payments against activities with partial allocations spanning multiple trips. Bookers, work queues and schedule views for the team that makes the reservations. Cash flow reporting, urgency-colored running totals. A schedule maker that generates PDF and DOCX itinerary outputs via background queue workers. Post-trip reconciliation panels. Template save-and-insert workflows. Clone and time-shift operations for duplicating trip structures. Canned trips for recurring program types. An audit log tracking every field change.
Email notifications came online May 13: status-change alerts, note broadcasts, date-shift announcements. All through Resend. Each email includes a before-and-after change log pulled from the audit trail.
The client vibecoded. The developer kept the architecture from collapsing under the velocity.
By May 12, the codebase was 26,000 lines across two monolith API files, a monolith activity editor component, and a monolith budget panel. It worked. It had also grown too large for two developers to navigate safely.The architecture audit
The developer wrote an architecture review prompt, 173 lines of markdown calibrated to the specific constraints: a two-person team, a single-tenant internal app, no test suite, deploy-first workflow to Cloudflare. The prompt instructed the reviewer to focus on code organization, duplication, and inconsistency, not to prescribe frameworks or architectural rewrites.
I ran the first audit on May 12. The findings were blunt: activities.ts at 1,646 lines. groups.ts at 1,438 lines. ActivityEditPanel.vue at 1,522 lines. BudgetSummaryPanel.vue at 1,070 lines. Dynamic UPDATE assembly duplicated seven ways. Error envelopes inconsistent across routes. Contact DTOs living inside a PC-screen composable. Bookkeeping payment mutations with no input validation. A schedule maker dialog that was a complete duplicate of the schedule maker panel.
I ran four more audits across the following four days. Each one re-verified the type checks and builds, inventoried file sizes, hunted for duplication, checked that findings were actually resolved. The audit deliverables became a feedback loop: findings found, remediations planned, code pushed, re-verified.
The refactoring
May 14 through 16 was cleanup at scale. The monoliths came apart.
activities.ts became a folder of fourteen files: activity routes, group-activity routes, bookkeeping entry routes, item routes, plus shared serializers, permission checks, field constants, audit helpers, data access, and bulk operations. No file over 400 lines.
groups.ts became seventeen files: CRUD, list, budget, post-trip, staff and hotels, clone, time-shift, import, templates, plus shared status normalization, date helpers, audit builders, and field lists.
ActivityEditPanel.vue dropped from 1,546 lines to 378. Six tab components extracted: details, booking, financial, notes, files, history. Three composables: form state, contact lookups, inline notes. The parent became a thin shell that delegates to specialized components.
BudgetSummaryPanel.vue dropped from 1,070 lines to 444. Pure math functions extracted to utils/budgetTiers.ts, unit-testable without Vue. Tier table, snapshot picker, and notes panel each in their own component.
Duplication that was fine for one sprint became unacceptable for two developers editing in parallel. The activity item catalog was duplicated between admin and non-admin routes. Email notification routes repeated the same 70-line send-or-skip skeleton three times. Contact editing duplicated form hydration, save logic, skills management, and delete behavior across two different editors. Each got extracted into shared modules, shared composables, shared types.
Bookkeeping got hardened: payment creates and updates now validate payloads through typed guards. Missing payment IDs return 404 instead of silently succeeding with zero changed rows. Activity status updates verify both the activity and the bookkeeping row exist before writing.
Performance work ran alongside the refactoring. Virtual scroll on the itinerary grid for trips with hundreds of activities. Composite list indexes dropping query times. Parallel activity pre-warming. A backfill migration for soft-deleted records. Sticky frozen column headers. Responsive drawer layouts for tablets.
Where we stand
The numbers, three weeks from first commit to architecture cleanup:
- 31,836 lines of TypeScript and Vue, across 48 API route files and 62 Vue components
- 20 database migrations, from initial auth schema through performance indexes
- 13 views plus an admin section: trips, trip detail, bookers, bookkeeping, cash flow, contacts, contact detail, speakers, audit log, access log, import, payment rules, permissions
- 14 feature modules: auth, trips, itinerary grid, budget, contacts, bookers, bookkeeping, cash flow, schedule maker, post-trip, email notifications, templates, import/export, audit log
- 5 architecture reviews, each re-verifying type checks and builds, driving the refactoring decisions
- Zero frameworks added. No ORM. No state management library beyond Pinia for auth. No runtime validation library. The codebase stayed thin: shared helpers, convention over configuration, allowlist-driven dynamic SQL assembly
The developer and the client worked in parallel throughout. The architecture reviews reduced the conflict surface. When both developers touch the same feature, the files they edit should be different files.
But this is not a finished project. The features are built and the code is clean, but the application has no real data in it yet. The FileMaker production database still runs the business. A significant body of work remains before go-live: end-to-end testing across every module, validation that the budget math matches the existing FileMaker calculations to the dollar, and the data migration pipeline that will extract, transform, and load decades of trip records into the new schema. The migrate-filemaker toolkit handles the DDR-to-schema translation, but the actual data pipeline — mapping legacy FileMaker field names to the new column structure, handling edge cases in twenty-year-old records, verifying referential integrity across imported foreign keys — is still ahead.
The codebase has a migration/ directory with an import pipeline script and a migration/10_d1_staging_import.md guide that lays out the D1 loading strategy. The schema baseline needs a refresh against the current migration chain. Auth session tokens need hashing. A fresh D1 database will receive the imported data, and the application will be repointed only after acceptance testing.
Why this matters for the practice
FileMaker migration is the developer's core service. Every migration follows the same progression: DDR analysis, schema design, API design, foundation build, feature sprint, architecture audit, refactoring. The tools change but the pattern holds.
This project is the fastest the developer has moved from intake to a clean, feature-complete codebase. The Cloudflare stack eliminated infrastructure decisions. The architecture reviews caught sprawl before it became debt. The agent-handoff system kept two developers synchronized without meetings. The open-source migrate-filemaker toolkit automated the DDR-to-plan pipeline.
The next chapters — testing, validation, data migration, go-live — will be written here. This post is a progress marker, not a conclusion.

