Platform architecture
How VENI-AI fits together — the plain-language picture first, then the engineering detail for anyone building on the platform.
In plain terms
Think of VENI-AI as a shopping mall. The building — one entrance, the security desk, the directory, the shared hallways — is the Shell. The shops inside are the apps: Drive, HRM, Document, the AI Assistant. Each shop runs its own business (its own staff, stock and layout), but shoppers come in through one door, and the mall handles security and the map.
So the whole platform comes down to three ideas:
- One front door — you sign in once at the Shell, and it takes you into any app.
- Independent apps — each app is built and run by its own team, with its own data, so they don't get in each other's way; if one breaks, the others stay open.
- Shared basics — the things everyone needs (knowing who you are, passing messages, storage) are provided once by the platform, not rebuilt by every app.
The diagram below shows exactly this: the Shell across the top, the apps as columns beneath it, and the lines are how they talk. Everything after this section is the engineering detail — for people building on the platform.
Glossary — the words used below, in one line each
- Shell — the host app (the "mall building"): sign-in, navigation, the app directory. Repo
veni-shell. - App / satellite — a plug-in application (Drive, HRM, …; the "shops"). Owns its own UI, API and database.
- SCS (Self-Contained System) — the rule that each app is fully independent: its own interface, logic and data.
- Launchpad — the Shell's home screen that lists and opens the apps.
- Module Federation — how an app's UI loads inside the Shell at runtime (one page, many apps).
- Connect RPC — how one server calls another directly: a typed API over HTTP.
- Event bus (Redis Streams) — a message stream; when something changes (e.g. a new user), each app hears it and updates its own copy.
- JWT — a signed token that proves who you are as requests pass between the Shell and an app.
- RBAC — role-based access control: the rules for what each user is allowed to do.
For builders
VENI-AI is an Agentic Enterprise OS built as a set of Self-Contained Systems (SCS): one Shell and a fleet of satellite apps. The Shell is the orchestrator and the single source of truth for users and organizations — it provides identity, navigation, an app registry, RBAC, billing and audit. Each satellite — Drive, HRM, Document, AI Assistant and more — is a full vertical slice that owns its own UI, API and database; everything domain-specific belongs to exactly one of them.
The pieces stay loosely coupled through three well-defined channels: Module Federation wires satellite UIs into the Shell at runtime, Connect RPC carries synchronous server-to-server calls, and a Redis Streams event bus propagates identity changes asynchronously.
Repositories
The Shell host app lives in its own repo, VENIZIA-AI/veni-shell. The veni CLI and the remote-app template ship from VENIZIA-AI/veni-builder, which bundles both and serves the installer — see Veni builder. The VENIZIA-AI/veni-ai monorepo holds shared gRPC contracts and shared infrastructure. Each satellite (HRM, Drive, …) lives in its own repository.
Self-Contained Systems
Each satellite is a Self-Contained System — a full web application, not a backend-only microservice — and owns all three pillars:
The point is decoupling: single-team ownership of the full stack, and failure isolation — a fault in HRM must not bring down Drive. "Independent" then holds at the application and the infrastructure level: each app owns its own database, object storage and workers, plus its Docker images, its Kubernetes manifests, its ArgoCD Application, and its own repository and release cadence. Only a few cross-cutting services — identity, the event bus, the service mesh and observability — are platform infrastructure shared by everyone and owned by no single SCS.
The first principle
When a boundary is unclear, distribute — split the concern into its own system rather than sharing a table or a service. Coupling is allowed at the infrastructure layer, never at the application layer.
The Shell and its satellites
The Shell is the platform's entry hub. It owns everything that must be global and consistent; satellites own everything domain-specific.
The Shell owns
- Identity — Keycloak / OIDC integration, RS256-signed SSO tokens, and a JWKS endpoint satellites verify against.
- App registry — a Postgres-backed catalog of available apps, their
remoteEntry.jsURLs, status and per-organization entitlements. - Navigation — the global launchpad / app rail and layout, filtered by permission.
- Governance — B2B onboarding, billing and subscriptions (Stripe), the global RBAC policy, and universal audit.
Satellites own a single business domain end-to-end, plus a local read-model copy of the user/org data they need, kept in sync via events.
| App | Kind | Status | Purpose |
|---|---|---|---|
| Shell | Core | Active | Platform orchestrator — identity, registry, onboarding, billing |
| Drive | Satellite | Active | Enterprise content management — MinIO/S3 storage, folders, sharing, quotas |
| HRM | Satellite | Active | People operations — employee lifecycle, org charts, leave, onboarding |
| Document | Satellite | Active | Collaborative rich-text workspace — approval workflows, versioning |
| AI Assistant | Satellite | Beta | Private RAG agent over the organization's knowledge base |
| Auto Report | Satellite | Planned | Cross-satellite data synthesis — executive summaries and PDF reports |
| Veni builder | Tool | Beta | The app scaffolder — generates new satellites from the template |
The Shell API runs on port 3000; the Shell UI on 5173 (dev) / 80 (container). Every port a local stack uses is in Ports at a glance.
Cross-system integration
Three channels keep the systems independent while still cooperating — each detailed below.
| Channel | Direction | Transport | Purpose |
|---|---|---|---|
| Module Federation | Shell → satellite | ES modules over HTTP | Mount a satellite's UI into the Shell at runtime |
| Connect RPC | Satellite ↔ Shell | Protobuf-JSON over HTTP | Token exchange, RBAC, quota |
| Event bus | Shell → satellites | Redis Streams | Replicate user/org changes into local read models |
Module Federation (UI)
Each satellite exposes a single federated module, ./ShellEntry. Rather than the Module Federation runtime's loadRemote() (which deadlocks Vite's sharing scope in dev), the Shell imports the remote's remoteEntry.js directly as an ES module and calls the standard container interface. Remotes bundle their own React and initialize with an empty shared scope — this avoids the "two React instances" hook-dispatcher crash. The Shell then calls the remote's mount(container, { apiBaseUrl, config }), handing it its API base URL and per-organization config.
Connect RPC (synchronous)
Server-to-server calls use Connect RPC — Protobuf contracts served as JSON-over-HTTP.
It is not a separate gRPC port
The "gRPC" handlers are Connect-wire routes mounted on the same Hono API server (port 3000) at paths like /{package}.{Service}/{Method}. There is no HTTP/2 gRPC endpoint to connect to.
| Service | Methods | Audience | Used for |
|---|---|---|---|
auth.v1.AuthService | ExchangeToken, VerifyToken | — | A satellite mints its own service JWT |
auth.v1.AuthGateway | ExchangeForService | — | Shell brokers an exchange to a target satellite |
rbac.v1.RbacService | GetUserPermissions, CheckPermission | shell-rbac | Ask the Shell to decide a permission (see the note below) |
quota.v1.QuotaService | CheckLimit, ConsumeUsage | shell-quota | Centralized quota enforcement |
Contracts are generated with buf (protoc-gen-es + protoc-gen-connect-es) into packages/grpc-contracts.
Event bus (asynchronous)
Identity data is replicated with an event bus built on Redis Streams (not Kafka). The Shell publishes domain events with XADD to two streams:
| Stream | Events |
|---|---|
shell:stream:users | user.created, user.updated, user.deleted |
shell:stream:organizations | organization.created, organization.updated, organization.deleted |
Each satellite runs an event subscriber that reads with XREADGROUP under its own consumer group (drive-api, hrm-api, …) and upserts into its own local read-model table (ON CONFLICT DO UPDATE). Delivery is idempotent and at-least-once. Drive subscribes to users only; HRM to users and organizations.
Authentication and authorization
The Shell is the central identity hub and the only component that signs platform tokens. Users sign in with local credentials, Google, or any OIDC provider — Keycloak is the standard OIDC integration. Whatever the sign-in method, the Shell mints one platform token, so satellites never deal with the upstream provider.
- The Shell mints RS256 JWTs (issuer
veni-ai-platform, audienceveni-ai-services) and publishes its public keys atGET /api/.well-known/jwks.json. Satellites share no secret — they verify Shell tokens against JWKS on their own. - The Shell is a BFF: its own access/refresh tokens live in HttpOnly cookies (
shell_access_token,shell_refresh_token).
Token exchange happens one of two ways — both are live in code:
- Direct — the remote calls
POST /api/auth/exchangewith the Shell JWT and the Shell mints a service-scoped token. - Brokered — the remote calls the
AuthGateway.ExchangeForServiceRPC and the Shell calls the target satellite's ownAuthService.ExchangeToken, so the satellite mints its own JWT.
The token-broadcast race
Same-origin remotes receive the Shell JWT over a BroadcastChannel (veni-ai-auth-channel). Because the Shell de-dupes identical tokens, a late-mounting remote can miss the initial broadcast. Every remote must call requestToken() right after subscribing — that posts an AUTH_TOKEN_REQUEST, and the Shell re-broadcasts with force=true. Skip it and the remote cold-loads against a stale cookie and reports "Not signed in."
Authorization is local to each system — there is no central arbiter, which is the SCS principle applied to permissions.
The Shell enforces its own with Casbin, behind an @authorize decorator on its controllers. Users hold roles (SYSTEM_ADMIN, ORG_ADMIN, member, …); permissions are resource:action and may be app-scoped as appId:resource:action. Checks resolve org-scoped roles plus system roles, so the model is multi-tenant. Policy is cached in Redis; logout blacklists the token.
A satellite decides for itself, from the identity the token gives it. The scaffold ships a small roles + permissions model whose codes ride in the service token and are checked by guards in the handler; an app whose rules outgrow flat codes brings its own engine — Casbin is a common choice — against its own tables. How a satellite does it →
RbacService is exposed, but nothing calls it
The Shell serves rbac.v1.RbacService (GetUserPermissions, CheckPermission) at /rbac.v1.RbacService/*, so a satellite could delegate its checks to the Shell. None does — every app authorizes locally. Treat the RPC as an available option, not the model.
Infrastructure
Each system runs its own infrastructure — this is the "independent at the infrastructure level" half of the SCS principle. Only a small set of cross-cutting services is shared by the whole platform.
Each system owns its own — isolated, never shared with another:
| Resource | Notes |
|---|---|
| Database | Its own PostgreSQL (a separate instance per app) — no shared tables |
| Object storage | Where it needs it — e.g. Drive's own MinIO / S3 bucket |
| Cache & queues | Its own Redis / job queues for its own workloads |
| Workers | Its own migration jobs, cron and event consumers |
Shared platform services — owned by no single SCS:
| Service | Role |
|---|---|
| Keycloak | Identity provider (OIDC) — single sign-on across every app |
| Observability | One monitoring stack for all — Prometheus, Grafana, Loki, Tempo, OpenTelemetry |
| Event bus | Redis Streams the Shell publishes user / org changes to |
| Service mesh | Linkerd — mTLS between services |
| Ingress + TLS | Nginx ingress + cert-manager (Let's Encrypt) |
| Secrets | Vault + the External Secrets Operator |
Rule of thumb
If it holds a system's data or serves its own workload, that system owns it. If it's identity, observability, networking, or the cross-system event bus, it's platform-shared.
Scaffolding a new system
New satellites are generated by the veni CLI (or the Builder wizard) from a shared template, so every SCS starts with identical auth, federation and data wiring — UI-only by default, with --api / --db and veni enable adding layers. Registration then writes a row into the Shell's app registry (POST /api/admin/apps).
The mechanics — every flag, the generated layout, the CLI-owned files you must not edit — live in Veni builder and Build a satellite.
Deployment
The platform runs on Kubernetes with GitOps via ArgoCD — an ApplicationSet generates one Application per (app, environment) pair and syncs each app's own deploy/k8s/overlays/<env> from git, so there is no kubectl apply in the pipeline. Releases promote feature/* → Dev → UAT → Prod through branches and tags, with Prod gated on the matching UAT-passed commit.
Deployment → — the full pipeline: the ApplicationSet, Kustomize overlays, secrets and migrations, the environments, and the promotion flow.
Where to go next
- Get started — run the whole thing locally, or against the hosted Dev Shell.
- Build a satellite — scaffold and grow your own app on the platform.
- Services — every service, its
docker-compose, ports and databases.