Architecture
Runtime services, tenant isolation, data flow, and deployment boundaries.
Runtime architecture
ProBeya is a TypeScript modular monolith with a separate realtime service:
Internet
|
v
Nginx (TLS and hostname routing)
|-----------------------------|
v v
Next.js 16 `web` Node.js `ws`
port 8000 port 8003
- React 19 UI - authenticated upgrades
- Auth.js - board rooms/presence
- tRPC and REST - Redis pub/sub fan-out
- item-event worker
|
+---------------+------------------+
v v v
PostgreSQL 16 Redis 7 S3-compatible storage
Drizzle + RLS cache/pub-sub private attachments
There is no separate application API process. Next.js hosts the tRPC routers
and HTTP routes implemented in packages/api. The production Compose stack
also builds the Docusaurus site and routes it through Nginx.
Repository boundaries
apps/
├── web/ Next.js UI, Auth.js, tRPC/HTTP runtime
├── ws/ Node.js WebSocket service
├── docusaurus-docs/ Generated product/operator site
└── mintlify-docs/ Developer documentation source
packages/
├── api/ Routers, permissions, workers, integrations
├── db/ Drizzle schema and database operations
├── shared/ Validators, constants, and shared types
├── ui/ Shared React components
└── config/ Shared TypeScript and lint configuration
docker/
├── docker-compose.yml Local infrastructure/base model
├── docker-compose.prod.yml Production overlay
└── Dockerfile.* Web, WS, docs, and schema images
Request and tenant lifecycle
- Nginx routes an HTTPS request to Next.js.
- The request layer classifies the host and Auth.js verifies the session.
- An authenticated tenant procedure resolves the organization from trusted
session and membership state. It never trusts a client-supplied
organizationId. - The procedure starts a PostgreSQL transaction and authorizes
SET LOCAL app.current_org_idagainst canonical membership data. - Router-level ACL checks restrict the requested workspace, board, item, and column operation.
- Drizzle executes queries with explicit organization predicates.
- Canonical
FORCE ROW LEVEL SECURITYpolicies restrictprobeya_appas a final database boundary.
Application processes must use the probeya_app runtime role. The
probeya_admin role can bypass RLS and is reserved for the one-shot schema
operation and controlled local administration.
The core hierarchy is:
organizations
└── workspaces
└── projects
└── boards
├── groups
│ └── items
│ └── values
├── columns
└── views
Tenant-scoped tables carry organization_id. Board reads and exports apply
board grants, workspace membership, item visibility, and hidden-column rules in
addition to the organization boundary.
API layer
The web application consumes tRPC v11 routers directly from packages/api, so
input validation and response types are shared end to end:
const board = await trpc.boards.getById.query({
boardId: "clx_board_packaging",
});
REST endpoints exist for external integration surfaces. Both REST and tRPC must enter the same authenticated tenant and permission boundaries.
Durable item side effects
Item create, update, move, value-change, and delete operations use a transactional PostgreSQL outbox:
- The item change and a versioned outbox event commit in one transaction.
- The worker running in the production
webprocess claims pending events with bounded locks. - Internal automation/notification state is processed in tenant context.
- External webhook, integration, and email effects carry stable event or idempotency metadata and retry with backoff.
- Failed events remain observable instead of being silently discarded.
The production Compose overlay sets ITEM_EVENT_OUTBOX_WORKER_ENABLED=true.
Local development leaves it disabled unless side-effect processing is under
test.
Realtime
apps/ws is a Node.js 22 HTTP/WebSocket process based on the ws package. It
rejects an upgrade unless a short-lived signed token is valid. Clients then use
typed messages such as join_board and leave_board; server authorization
checks the immutable token claims before room changes.
Redis pub/sub fans board updates across WebSocket instances. GET /health
reports WebSocket/Redis readiness for the container health check.
Storage
The object store uses two different origins:
S3_ENDPOINTis reachable by the server and may usehttp://minio:9000.S3_PUBLIC_URLis reachable by browsers and must use the public TLS hostname.
The bucket is private. Anonymous reads are limited to explicit avatar and logo prefixes; attachments are downloaded only through ACL-checked signed URLs. Production upload writes fail closed if ClamAV is unavailable.
Search
The current search path queries PostgreSQL in the authenticated tenant context. Results are filtered by organization, workspace/board ACLs, item visibility, and hidden-column rules before return. The supported Compose stack does not include Meilisearch.
Deployment
The supported self-hosted model combines the base Compose file with its
production overlay. It builds web, ws, and docusaurus, plus an explicit
one-shot schema image. Database changes use the operator-controlled sequence:
stop writes -> verified backup -> ownership adoption
-> db:migrate -> apply-rls -> check-rls -> up
See Self-Hosting for the executable runbook.
Was this page helpful?