# Nexus Suite

*A client's whole business ran on Bitrix. The rebuild had to match behaviour nobody wrote down.*

**Category:** WEB PLATFORM / CRM
**Status:** internal
**Period:** June – July 2026
**Built by:** HM Ammar, Digital Architect & Head of IT, Kuala Lumpur

**9,628** — rows of Bitrix's permission matrix, decoded without a write

**Stack:** TypeScript, Next.js 14, NestJS, PostgreSQL 16/17, Row-Level Security, pnpm workspaces, Vitest, pm2

## The challenge

A client ran their whole business on a self-hosted Bitrix24 portal: deals, telephony, messaging, payment integrations, per-stage automation robots. My brief was to rebuild all of it. The CRUD was never the hard part. Bitrix's real behaviour — how a permission scope resolves, what a stage change actually starts, which column a deal lands in — is undocumented and lives in PHP source and MySQL tables, and guessing at it produces something that looks like Bitrix and behaves like nothing.

## What I built

I built the spine first: a pnpm monorepo with a Next.js 14 dashboard, a NestJS API and a core package holding the business logic, on Postgres with row-level security on every table. It runs to roughly 211,000 lines of TypeScript, 148 hand-written SQL migrations, 109 API controllers, 110 core services and 71 dashboard screens, covered by 274 test files — every database-touching one of them against a live Postgres rather than a mocked driver. I put every external provider (email, SMS, payments, telephony, LLM) behind an adapter that resolves to a mock under BUILD_MODE=safe, so I built and deployed the platform without a single real send, charge or call. Three weeks in, with the spine standing, the open question was what Bitrix actually does, so I audited the portal from the inside under a read-only rule. That audit became my spec for the parity push and the roadmap after it.

## The detail

### Fail-closed tenancy

Two Postgres roles, and I deliberately made the request-serving one not the table owner, not a superuser and not BYPASSRLS, so it has no route around RLS. The policy reads tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::uuid: on a pooled connection where the setting was never applied, the empty string coerces to NULL and the comparison returns no rows instead of raising an invalid-uuid error. It fails to silence rather than to a leak — run it locally and the app role sees zero tenants where the owner role sees two.

### Read-only forensics

I audited the Bitrix portal over SSH under one hard rule: SELECT, SHOW and DESCRIBE, never a write to a client's live database. It produced about seventy artefacts, 45 analysis documents plus 24 raw table dumps, including the permission scope ladder I recovered from source (A=Self, D=Dept, F=Dept+subs, O=Open, X=All) and a decoded 9,628-row role-permission matrix. The most useful finding was that the CRM robot board is deprecated and delegates to the bizproc workflow runtime, which settled the design: one durable runtime with two editors over it, not two engines.

### Agents under RBAC

An AI teammate is not a special case in my authorisation code. It is a row in users with is_ai_agent=true, so the same principal loader resolves it, RLS scopes it, and role grants bind every action it takes. A new agent starts with no grants and can see and do nothing until an admin assigns roles; anything the guardrails mark require_human lands in a pending_agent_actions queue instead of executing.

### At-least-once outbox

State change and event row commit in one transaction, and a dispatcher drains them with FOR UPDATE SKIP LOCKED so several instances can run at once without double-publishing or blocking each other. Publish happens before the published_at stamp commits, so delivery is at-least-once by choice. I wrote that trade-off into the decision log with its reason: no-loss beats no-duplication, and exactly-once across a publish and a database commit is not reachable without idempotent consumers anyway.

### Deploying without root

The target box already ran seven other projects and my deploy user had no passwordless sudo, so I put everything into userspace: a dedicated Postgres cluster on port 5433 bound to localhost, free high ports for API and web, pm2 with pm2 save for restart-on-boot. On the server the Next config self-proxies /v1 to the API on 127.0.0.1 with NEXT_PUBLIC_API_URL left empty, so the same build works on a bare address now and behind a domain later with no rebuild, no CORS and no mixed content. I left the seven existing apps untouched and their uptimes unchanged.

## The outcome

I built it, deployed it, and it runs on an internal server with every provider mocked. There is no public domain, no TLS, and no real user has ever logged in. The most useful output may be the parity assessment I commissioned against my own build: of roughly 450 Bitrix functions scored, about a third are at parity, a third partial — mostly engines built but not yet wired to a screen — and a third missing or shallow. That verdict is self-assessed, it is unflattering, and it is what I am building the remaining roadmap from.

---

[All work](https://hmammar.world/) · [HTML version](https://hmammar.world/work/nexus-suite.html)
