02 / Architecture

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:

Pillar 1 · UI
Own UI
React + Vite, exposed as a Module Federation remote (also runnable standalone). Provides pages, mounted into the Shell at runtime.
Pillar 2 · API
Own API
Its own Bun + Hono + Ignis server, issuing its own JWTs. Talks to others only via contracts — never direct DB access.
Pillar 3 · Data
Own data
Owns its schema and Drizzle migrations. No shared tables, no cross-satellite reads or writes.

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.js URLs, 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.

AppKindStatusPurpose
ShellCoreActivePlatform orchestrator — identity, registry, onboarding, billing
DriveSatelliteActiveEnterprise content management — MinIO/S3 storage, folders, sharing, quotas
HRMSatelliteActivePeople operations — employee lifecycle, org charts, leave, onboarding
DocumentSatelliteActiveCollaborative rich-text workspace — approval workflows, versioning
AI AssistantSatelliteBetaPrivate RAG agent over the organization's knowledge base
Auto ReportSatellitePlannedCross-satellite data synthesis — executive summaries and PDF reports
Veni builderToolBetaThe 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.

ChannelDirectionTransportPurpose
Module FederationShell → satelliteES modules over HTTPMount a satellite's UI into the Shell at runtime
Connect RPCSatellite ↔ ShellProtobuf-JSON over HTTPToken exchange, RBAC, quota
Event busShell → satellitesRedis StreamsReplicate 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.

ServiceMethodsAudienceUsed for
auth.v1.AuthServiceExchangeToken, VerifyToken—A satellite mints its own service JWT
auth.v1.AuthGatewayExchangeForService—Shell brokers an exchange to a target satellite
rbac.v1.RbacServiceGetUserPermissions, CheckPermissionshell-rbacAsk the Shell to decide a permission (see the note below)
quota.v1.QuotaServiceCheckLimit, ConsumeUsageshell-quotaCentralized 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:

StreamEvents
shell:stream:usersuser.created, user.updated, user.deleted
shell:stream:organizationsorganization.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, audience veni-ai-services) and publishes its public keys at GET /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/exchange with the Shell JWT and the Shell mints a service-scoped token.
  • Brokered — the remote calls the AuthGateway.ExchangeForService RPC and the Shell calls the target satellite's own AuthService.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:

ResourceNotes
DatabaseIts own PostgreSQL (a separate instance per app) — no shared tables
Object storageWhere it needs it — e.g. Drive's own MinIO / S3 bucket
Cache & queuesIts own Redis / job queues for its own workloads
WorkersIts own migration jobs, cron and event consumers

Shared platform services — owned by no single SCS:

ServiceRole
KeycloakIdentity provider (OIDC) — single sign-on across every app
ObservabilityOne monitoring stack for all — Prometheus, Grafana, Loki, Tempo, OpenTelemetry
Event busRedis Streams the Shell publishes user / org changes to
Service meshLinkerd — mTLS between services
Ingress + TLSNginx ingress + cert-manager (Let's Encrypt)
SecretsVault + 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.