Acrosstek CRM: building enterprise software for a real client
A commissioned enterprise CRM with real-time collaboration, offline-first mobile, and a scope that grew 3x. Here is what I learned about client projects and the architecture decisions I would change.
Acrosstek CRM: building enterprise software for a real client
Acrosstek CRM was a commissioned project. Enterprise CRM with custom features, real-time collaboration, and an offline-first mobile app. The kind of project where the feature list is 50 pages long and the timeline is optimistic. I shipped it. I also made several architecture decisions I would do differently now.
the architecture
Monorepo with clear package boundaries:
packages/
core/ domain models and business logic, zero framework dependencies
api/ Express plus WebSocket server
web/ React admin dashboard
mobile/ React Native companion app
sync/ offline sync engine (CRDT-based)
The monorepo structure was the right call. Shared types between web and mobile meant API changes broke both clients at compile time instead of at runtime. The core package with zero framework dependencies meant business logic was testable without spinning up a server or a database.
what worked
TypeScript across the board. This one was obvious in retrospect but felt like a bet at the time. The mobile app shares the same types, validators, and API client code as the web dashboard. When you add a field to an API response, the TypeScript compiler tells you everywhere it needs handling. This alone prevented whole categories of bugs.
The offline sync engine was technically interesting. The mobile app needed to work without internet because salespeople in basements, factories, and airplanes are real users. We implemented CRDT-based sync: each device stores data locally and syncs when connected. Conflicts resolve automatically. The "edited the same deal on phone and laptop" scenario handles cleanly.
PostgreSQL with TypeORM handled everything we threw at it. The key is knowing when to drop to raw SQL. Complex reporting queries with multiple aggregations and window functions: raw SQL. Standard CRUD operations: TypeORM. Following that rule kept the ORM layer from becoming a crutch while keeping the simple paths simple.
what did not
The scope grew roughly 3 times from the original spec. Custom dashboards. Role-based access control. A report builder. Email templates. A document attachment system. Each feature is small on its own. Together they are a different product than the one quoted. Managing this without a clear change order process was painful and unprofitable.
The CRDT-based sync engine was overengineered for the actual use case. The client had 30 users. A simpler "last-write-wins with server timestamps" would have worked fine. CRDTs are powerful but they add complexity, and complexity has a maintenance cost that outlasts the initial build.
React Native for the mobile app was a mistake for this specific project. Heavy form inputs, camera integration, offline storage. React Native adds a layer of abstraction that makes all three harder. The forms needed custom native components anyway. A native Kotlin app would have been more work upfront but less maintenance over 18 months.
lessons for client projects
Fixed-price contracts with changing scope are a trap. Either bill by the hour or have a written change order process with costs attached. The middle ground where you just absorb scope creep is not sustainable.
Build the CI/CD pipeline in the first sprint, not the last. Waiting until "the end" to set up deployment means everything ships late. Infrastructure is not optional.
Document every significant decision in writing. Enterprise clients change personnel. The person who approved the architecture in January is gone by April. Written decisions survive personnel changes. Verbal agreements do not.
The client does not care about your architecture. They care about whether the software works. Keep the architecture clean for your own sanity, but ship features. The cleanest codebase that ships nothing is worth nothing.
The codebase is private. But if you are doing enterprise client work, the lessons are public: scope strictly, TypeScript everywhere, think twice before React Native, and document everything.