Soul Global 2.0

Soul Global 2.0

A full architectural rewrite of Soul Global β€” from a React + REST monolith to Next.js 16 Server Components with durable workflows.

Version 1 proved the product but accumulated client-side complexity and a brittle order model. Version 2 is a ground-up rewrite on a server-component-first architecture, with Inngest workflows and a complete Brazilian import tax engine. The hard part was migrating live production data while v1 kept serving paying customers.

ROLE

Software Engineer

Full Stack

Architect

TOOLS

Bun

Next.js 16

React 19

PostgreSQL

Drizzle ORM

Inngest

Decimal.js

next-intl

DURATION

2025 - Present

INTEGRATIONS

Supabase

Siscomex (Portal Único)

Asaas

ZapSign

ShipsGo

Anthropic Claude

🌐

soulglobal.com.br

✢ The system, end to end

Four personas hit one App Router, which talks to a service layer, a pure pricing engine, 55 Postgres tables and eight external systems β€” while inbound webhooks feed 18 durable Inngest functions. Tap any node to open it.

✢ Live data migration (11-step β†’ 7-step)

Migrated the legacy schema to a simpler order model while preserving every historical order, contract, and payment β€” as two versioned pipelines of idempotent phases, with the old-to-new id map kept in a real table so a crash never means starting over.

Legacy migration as a re-runnable pipeline

migrate-legacy Β· 8 phases

01-foundation02-auth-orgs03-products04-logistics05-quotes06-shipments07-financial08-system

migrate-v2 Β· 6 phases

01-foundation02-auth-orgs03-suppliers04-shipments05-documents06-financial

_migration_id_mapping

legacy id

β†’

new uuid

primary key (entity_type, old_id) β€” a real table, not an in-memory Map, so the mapping survives a crash and a restart resumes where it stopped

idempotentre-runnablecrash-saferepair scripts
Two versioned pipelines, not one throwaway script. Each phase is idempotent, so a crash halfway through is fixed by running it again β€” the id map is already in the database.

✢ Client-fetching β†’ React Server Components

Re-architected to Next.js 16 Server Components by default, dropping initial load weight and reworking auth and caching that were core to v1's client-side SWR model.

✢ Durable workflows with Inngest

Replaced manual state machines and fragile cron jobs with 18 Inngest functions β€” serialised one-per-shipment, retried three times, idempotent by design, so payment reconciliation and customs progression survive crashes and 3rd-party outages.

Inbound webhook β†’ durable workflow

webhook

zapsignasaasshipsgo

verify + look up local row

inngest.send(event)

shipment-step-evaluator

concurrency: { key: shipmentId, limit: 1 }

retries: 3 Β· idempotent guarded UPDATE

step.sleepUntil(eta) β€” no cron polling

postgres tx

in-app notify

whatsapp

shipment state machine

payment→
shipping prep→
documents→
customs→
completion
Webhook routes are excluded from the auth proxy matcher β€” they carry no user session and verify themselves. Every side effect lives inside a step.run(), so a retry replays only the step that failed.

✢ Brazilian import tax engine

Full CIF β†’ II β†’ IPI β†’ PIS/COFINS β†’ Siscomex β†’ AFRMM β†’ ICMS cascade, including the "por dentro" gross-up, computed with Decimal.js so rounding never drifts.

Import tax cascade β€” one line item

CIF

100.000,00

capatazia / THC stay out of the base β€” Decree 11.090

II Β· 16%

16.000,00

IPI Β· 9,75%

11.310,00

the IPI base already carries the II

PIS Β· 2,10%

2.100,00

no ICMS in the base β€” STF, RE 559.937

COFINS Β· 9,65%

9.650,00

Siscomex

154,23

grouped by NCM + manufacturer, not per line item

AFRMM

960,00

ICMS base

140.174,23

ICMS Β· 18% por dentro

30.769,95

the tax sits inside its own base β€” a gross-up, not a flat 18%

landed cost

170.944,18

A naive ICMS (base Γ— 18%) returns 25.231,36 β€” off by 5.538,59 on a single item, then multiplied across every line of every invoice.

Every step in Decimal.js with ROUND_HALF_UP applied per stage. The engine imports no database code and is unit tested in isolation.

✢ Four personas on one database

Importers, Chinese sourcing partners, customs brokers and platform staff each get their own route group, gated in the layout before any child renders and backed by a signed active-organization cookie.

Four personas, one database

(admin)

platform staff

systemRole = SUPER_ADMIN

| SUPER_ADMIN_EMPLOYEE

every organization, rate tables, audit logs

(dashboard)

importer

signed org cookie

+ membership row

own quotes, shipments, invoices and documents

(china)

sourcing partner

…all of the above

+ isChinaPartner

only sourcing requests assigned to that partner

(broker)

customs broker

…all of the above

+ broker org

shipments where it is the customs broker, at customs stage

active org resolution

cookie active_organization_id

+ active_organization_sig β€” hmac-sha256, constant-time compare

tampered or stale β†’ redirect /api/clear-org

one shipments row, three organization roles

shipments

β†’asSellerorganizations
β†’asClientorganizations
β†’asCustomsBrokerorganizations
The gate lives in each route group's layout.tsx as an async Server Component β€” it resolves before any child renders, so an unauthorised page never reaches the client. Services re-check ownership on every call.