Stack Transformation

WorkSpaceMan (Current)

RuntimeNode 8/16 (EOL)
FrameworkJXP v2.0.0 + Express
DatabaseMongoDB (Mongoose)
APIAuto-CRUD, no versioning
AuthJWT (unverified), API keys
FrontendHandlebars + Svelte + Vue
QueueGoogle Pub/Sub
SearchElasticsearch
HostingVPS + Docker + Nginx
AINone
Models88 (all auto-exposed)

Proximity Green (Target)

RuntimeNode 22+ / Workers
FrameworkHono + Drizzle ORM
DatabasePostgreSQL (Neon)
APIREST v1 + OpenAPI 3.1
AuthClerk (OAuth 2.0 + SALTOK)
FrontendNext.js / SvelteKit
QueueInngest (typed events)
SearchTypesense
HostingCloudflare Workers + Pages
AIClaude API (Haiku + Sonnet)
Models~45 tables (normalised)

Data Model Migration: 88 Models → ~45 Tables

The 88 MongoDB models at api.workshop17.co.za map to ~45 PostgreSQL tables. Many WSM models consolidate, merge, or become columns on parent tables in a properly normalised relational schema.

Core Entities (direct mapping)

MongoDB ModelPostgreSQL Table userusers organisationorganisations locationlocations space + spacetypespaces room + bookableitemtype + layoutrooms booking + deskbookingbookings membershipmemberships licenselicenses checkincheckins guestguests product + producttypeproducts

Financial (normalised)

MongoDB ModelPostgreSQL Table invoiceinvoices lineitem + sentlineitem + adhocinvoice_line_items payment + paymentmethod + paymentrequestpayments + payment_methods ledgerledger_entries (immutable) wallet + wallettype + balancewallets (balance derived from ledger) currencycurrencies discountdiscounts invoicecomm + invoiceholidayinvoice_config (jsonb)

CRM (consolidated)

MongoDB ModelPostgreSQL Table lead + leadsource + leadtypeleads opportunityopportunities task + tracktasks + task_templates contact + crmnotecontacts + notes proposal + contract + businesscaseproposals (type enum) partnerpartners

Infrastructure (adapter pattern)

MongoDB ModelPostgreSQL Table claylock + clayaccessgroup + claytagaccess_credentials (polymorphic) radacctnetwork_accounts parkingcenter + parkingtenant + parkinguserparking_facilities + parking_allocations notificationparking_events

Models that become columns or are eliminated

MongoDB ModelIn PostgreSQL
balanceEliminated — derived from SUM(ledger_entries)
sentlineitemMerged into invoice_line_items with sent_at timestamp
tagPostgreSQL array column on users
industrysectorEnum or lookup table
countries, language, i18nStatic config / i18n library
spacetype, producttype, wallettypeEnum columns on parent tables
source, link, pinColumns on parent entities
appupdateEliminated — handled by deployment pipeline
config, instancesettingsEnvironment variables + settings table
scheduleEliminated — Inngest handles scheduling (no eval)
xeroaccount, xeroorgaccount, xerocreditnote, xeropaymentAccounting adapter state in accounting_sync table
payfasttoken, zapperPayment adapter state in payment_tokens table
apikey, token, refreshtoken, handovertokenManaged by Clerk auth provider

Migration Phases

Phase A: Data Export & Transform

Weeks 17–18 of build (parallel with Phase 5 frontend work)

Extract all data from the live MongoDB at api.workshop17.co.za and transform into PostgreSQL-compatible format.

Approach

  • Use the existing JXP API to export data — GET /api/{model}?limit=10000 for each of the 88 models. The API already supports pagination and field selection.
  • Claude Code writes the transformation scripts: one per model group (core, financial, CRM, infra)
  • Transformations handle: ObjectID → UUID mapping, denormalised fields → foreign keys, Mixed/schemaless fields → typed columns or jsonb

Key Transformations

ChallengeApproach
ObjectID referencesBuild a global ID map (old ObjectID → new UUID). Process models in dependency order.
Encrypted PINs/passwordsDecrypt with legacy cipher (see Fix 1.3), re-encrypt with new method for PostgreSQL.
Mongoose Mixed fieldsStore as jsonb columns in PostgreSQL. Migrate to typed columns in future iterations.
Computed fields in hooksRecompute from source data during migration. Don't trust cached values (e.g. balance).
Xero IDs everywherePreserve in accounting_sync table for continuity. New records use the accounting adapter.
Wallet balancesReconstruct from ledger entries, not from the balance model. The balance model is a cache.
Risk: The ledger model is the financial source of truth. If any ledger entries are missed or incorrectly mapped, wallet balances will be wrong. Mitigation: reconcile every user's balance post-migration against the WSM balance endpoint.

Phase B: API Compatibility Layer

Week 18 (1 week, built by Claude Code)

Build a thin translation layer that accepts JXP-style requests and routes them to the new PostgreSQL-backed API. This allows any existing integrations or scripts that hit api.workshop17.co.za to keep working during transition.

What It Translates

JXP PatternNew API Equivalent
GET /api/user?filter[status]=activeGET /api/v1/users?status=active
?autopopulate=1?include=organisation,location,membership
?filter[amount]=$gte:1000?amount_min=1000
POST /query/invoiceGET /api/v1/invoices with query params
POST /aggregate/ledgerGET /api/v1/reports/ledger-summary
_id (ObjectID)id (UUID) with old-to-new mapping
This layer is temporary (removed after cutover) but critical for zero-downtime migration. It can run as a Cloudflare Worker in front of the new API.

Phase C: Parallel Run & Reconciliation

Weeks 19–20 (2 weeks)

Both systems run simultaneously. The new platform handles all reads and new writes. The old system remains available as a fallback.

Parallel Run Rules

  • DNS: New platform goes live on a subdomain (e.g. app.proximity.green) while my.workspaceman.nl stays active
  • Reads: New platform serves all read traffic from PostgreSQL
  • Writes: New platform is the primary. Critical writes (payments, invoices) also logged to reconciliation queue
  • Reconciliation: Daily automated check comparing user counts, wallet balances, invoice totals, and booking counts between old and new

Daily Reconciliation Script

// Runs via Inngest every 24 hours
async function reconcile() {
    const oldUsers = await fetch('https://api.workshop17.co.za/count/user');
    const newUsers = await db.select(count()).from(users);

    const oldInvoiceTotal = await fetch(
        'https://api.workshop17.co.za/aggregate/invoice',
        { body: [{ $group: { _id: null, total: { $sum: "$total" } } }] }
    );
    const newInvoiceTotal = await db
        .select(sum(invoices.total))
        .from(invoices);

    // Compare and alert on discrepancies > 1%
    if (Math.abs(oldTotal - newTotal) / oldTotal > 0.01) {
        await alert('Invoice total mismatch', { oldTotal, newTotal });
    }
}

Rollback Criteria

  • If balance discrepancies exceed 1% for any user — pause and investigate
  • If payment processing fails on the new platform — redirect payments to old system
  • If more than 5 user-reported issues in first 48 hours — rollback DNS

Phase D: Cutover & Decommission

End of Week 20

Switch all traffic to the new platform and decommission WorkSpaceMan.

Cutover Sequence

  1. Final data sync: export any records created in old system during parallel run
  2. DNS switch: api.workshop17.co.za → compatibility layer on new platform
  3. Member portal: my.workspaceman.nl redirects to app.proximity.green
  4. CRM: crm.workspaceman.nl redirects to new CRM interface
  5. Monitor: 48-hour hypercare with on-call developer
  6. Remove compatibility layer after 30 days (all consumers migrated)
  7. Archive old MongoDB data (encrypted backup, retain for 12 months)
  8. Decommission old VPS infrastructure
Post-migration: The old api.workshop17.co.za docs become a historical reference. The new platform generates its own OpenAPI 3.1 spec at /api/v1/docs with interactive Swagger UI.

AI Features Activated at Launch

The new platform ships with AI capabilities from day one. Here's what's available at cutover vs what comes later:

At Launch (Week 20)

FeatureAI ModelWhat It Does
Member AssistantClaude Haiku 4.5Conversational booking, balance queries, team management
Smart BookingClaude Haiku 4.5Room recommendations based on history, preferences, availability
Admin BriefingClaude Sonnet 4.6Daily summary of priorities, anomalies, and action items
Invoice AnomaliesClaude Sonnet 4.6Flags unusual charges before invoice approval

Post-Launch (Months 2–3)

FeatureAI ModelRequires
Churn PredictionClaude Sonnet 4.62+ months of check-in and payment data in new system
Lead ScoringClaude Sonnet 4.6Historical conversion data from CRM migration
Predictive OccupancyClaude Sonnet 4.63+ months of booking patterns in new system
Dynamic PricingClaude Sonnet 4.66+ months of demand data

Integration Migration Map

Each external integration migrates through the adapter pattern. Existing service connections continue working; only the internal wiring changes.

IntegrationWSM ImplementationProximity GreenMigration Impact
XeroDirect API calls throughout codebaseAccountingProvider adapterRe-auth OAuth tokens for new platform. All Xero IDs preserved.
PayFastDirect API + stored tokensPaymentProvider adapterStored tokens migrate to payment_tokens table. Customers don't re-enter cards.
FreeRADIUSOpenRadius REST APINetworkAccessProvider adapterSame RADIUS server, new API client. Users keep WiFi credentials.
PaperCutSSH commands (vulnerable)PrintProvider adapter (REST)Switch from SSH to PaperCut REST API. Print balances preserved.
Clay locksDirect model storageAccessProvider adapterAccess groups and tags migrate. Physical hardware unchanged.
Admyt parkingREST APIParkingProvider adapterSame API, new client. Car registrations preserved.
Google Pub/SubMessage queue for jobsReplaced by InngestNo migration needed — new event system built from scratch.
ElasticsearchFull-text searchReplaced by TypesenseRe-index from PostgreSQL. No data migration — search index rebuilt.
SentryError trackingSentry (same)New DSN, same service. Historical errors stay in old project.
SMTP (email)NodemailerReplaced by ResendEmail templates recreated in new system. SMTP credentials not needed.

Migration Timeline

WeekPhaseKey ActionRisk Level
17A: ExportExtract all 88 models via JXP API, begin transformationLow
18A+B: Transform + CompatLoad into PostgreSQL, build compatibility layerMedium
19C: ParallelBoth systems live, daily reconciliation, UATMedium
20D: CutoverDNS switch, 48-hr hypercare, decommission oldHigh

Success Criteria