βΆ Try the interactive demo β sign in as each role and watch the authorization bite. Runs entirely in the browser on mock data; the real system needs Docker.
A full-stack demo system built around asynchronous service communication: RabbitMQ (async publish + RPC request/reply), PostgreSQL logging with JSONB and a trigger/procedure, document analysis powered by Google Gemini, and a Next.js panel whose every screen is gated by permissions the server actually enforces.
| Layer | Technology |
|---|---|
| Frontend | Next.js (React + TailwindCSS) |
| Backend | Node.js + Express |
| Database | PostgreSQL (JSONB, triggers, procedures, pgvector) |
| Message Broker | RabbitMQ (async + RPC) |
| Cache | Redis (optional) |
| AI | Google Gemini (file analysis) |
| Containers | Docker & Docker Compose |
- Authentication & dynamic RBAC: JWT + bcrypt login, with permissions stored in
role_permissionsrather than hard-coded. Every guarded route resolves the caller's permissions from the database on each request, so a Supervisor granting or revoking a permission throughPUT /admin/permissions/:roleIdtakes effect on the very next call β the user does not need to log in again. - RabbitMQ + RPC:
GET /rpc-testpublishes a message totask_queuewith areplyToqueue and acorrelationId, then waits for the consumer's reply β a full request/reply round trip. - PostgreSQL: Records are stored in a
JSONBcolumn; anAFTER INSERTtrigger calls aplpgsqlprocedure that writes an audit row automatically. - Gemini AI: An uploaded
.txtfile is summarized by Gemini, with fallback across model versions. - Panel bound to the real API: The Next.js frontend logs in, stores the JWT and reads its
permissions from
GET /auth/meβ never from the token payload. Each feature card declares the permission code it needs; a card the caller cannot use renders locked, with a button that fires the request anyway and prints the server's403, so the enforcement is visible rather than claimed. - Retrieval-augmented answers: Documents are split into overlapping chunks, embedded, and stored
in
pgvector. A question is embedded too, the nearest chunks are retrieved by cosine distance, and only those passages are given to the model β which then has to cite them. The retrieved passages come back with the answer, because otherwise a reader cannot tell a grounded answer from a fluent invention.POST /documents/searchexposes the retrieval half on its own, so the ranking can be inspected directly rather than guessed at through the generated text. - Redis caching: The per-request permission lookup is served from Redis when it is available.
The interesting part is what the cache must not break β a Supervisor's edit still has to apply to
the very next request β so
setRolePermissionsdrops the role's key after the transaction commits, and only that role's key. The cache is optional: with noREDIS_URLevery lookup goes to PostgreSQL, and if Redis fails mid-request the read falls through rather than erroring. - Docker Compose: Postgres, RabbitMQ, Redis, backend and frontend start with one command, with healthchecks so the backend waits until its dependencies are actually ready.
Being explicit so nobody is misled:
| Area | Status |
|---|---|
| RabbitMQ async + RPC | β
Implemented (src/config/rabbitmq.js, src/routes/rpc.routes.js) |
| PostgreSQL JSONB + trigger + procedure | β
Implemented (db/schema.sql) |
| Gemini document analysis | β
Implemented β .txt only |
| Docker Compose orchestration | β Implemented |
| Authentication (JWT + bcrypt) | β Implemented |
| Server-enforced dynamic RBAC | β Implemented β permissions live in the database and are checked per request |
| Automated tests + CI | β 99 integration tests against a real PostgreSQL, run on Node 20 and 22, twice β with and without the cache |
| Frontend wired to real auth | β Implemented β login, token storage, and permission-gated panels |
| Redis caching | β Implemented β optional, in front of the permission lookup, invalidated on write |
| RAG / embeddings | β Implemented β pgvector store, chunked documents, cited answers |
| RabbitMQ paths in CI | β¬ Not yet β covering them needs a broker service container; only their failure paths are tested today |
All routes except /health and /auth/login require an Authorization: Bearer <token> header.
Permission codes are enforced server-side.
Note that /auth/register is not open self-service. It takes the new user's role from the
request body, so leaving it unauthenticated would let anyone hand themselves a Supervisor account and
rewrite the permission matrix. Creating accounts is an administrative act and sits behind
users:write; the first Supervisor is seeded by npm run setup-db, and there is deliberately no
bootstrap path through the API.
| Method | Route | Required permission |
|---|---|---|
GET |
/health |
β |
POST |
/auth/login |
β |
POST |
/auth/register |
users:write |
GET |
/auth/me |
authenticated |
GET |
/records |
records:read |
POST |
/records |
records:write |
POST |
/ai-analyze |
ai:analyze |
GET |
/rpc-test |
rpc:execute |
GET |
/admin/permissions |
permissions:manage |
PUT |
/admin/permissions/:roleId |
permissions:manage |
GET |
/admin/users |
users:read |
GET |
/documents |
documents:read |
POST |
/documents |
documents:write |
DELETE |
/documents/:id |
documents:write |
POST |
/documents/search |
documents:read |
POST |
/documents/ask |
documents:read |
Default roles: Supervisor (all permissions), School (read/write/analyze), Company (read/analyze/rpc), Student (read/analyze).
Permission codes: records:read, records:write, ai:analyze, rpc:execute,
permissions:manage, users:read, users:write, documents:read, documents:write. They are rows
in the permissions table, so a Supervisor edits who holds what at runtime rather than through a
redeploy.
GEMINI_API_KEY gets gemini-embedding-001, asked for 768 dimensions to fit the vector(768)
column and renormalised afterwards, since truncating an embedding costs it its unit length.
Without a key the app falls back to a deterministic hashing embedder. Be clear about what that is: it matches documents that share words, not documents that share meaning β "car" and "automobile" are as unrelated to it as "car" and "banana". It exists so the pipeline (chunking, storage, cosine search, ranking, citations) can be exercised in CI and by anyone who clones the repo, and the panel labels it as a fallback rather than passing it off as semantic search.
Every chunk records the model that embedded it, and searches filter on that. Vectors from different models occupy unrelated coordinate spaces, so mixing them does not return worse results β it returns confidently ranked nonsense.
index.js entry point β connects services, starts the server
src/
app.js builds the Express app (exported so tests can drive it)
config/db.js PostgreSQL pool
config/rabbitmq.js broker connection + task_queue consumer
config/redis.js optional cache; every helper degrades to a no-op
services/embedding.service.js Gemini or offline embeddings, same dimensions
services/document.service.js chunking, ingestion, cosine search
middleware/auth.js authenticate() and requirePermission()
middleware/errorHandler.js
routes/ auth Β· records Β· ai Β· rpc Β· admin
services/permission.service.js
db/schema.sql full schema, idempotent
scripts/setup-db.js applies the schema, seeds the first Supervisor
tests/ integration suite + globalSetup/globalTeardown
frontend/
src/lib/api.js fetch wrapper β attaches the token, normalises errors
src/lib/auth-context.js session state; permissions come from GET /auth/me
src/app/login/page.js login screen
src/app/page.js dashboard
src/components/ Panel (permission gate) Β· matrix Β· records Β· rpc Β· ai
npm test # 91 tests; the 8 cache tests skip without Redis
# Same suite with the cache in front of every permission lookup β 99 tests.
# Use a dedicated database index so a run cannot evict a dev server's entries.
TEST_REDIS_URL=redis://localhost:6379/15 npm test
npm run test:coverage # with a coverage reportCI runs both passes on Node 20 and 22. The second one is the real cache test: if invalidation were wrong, the existing authorization assertions would start failing rather than some cache-specific assertion.
Run npm run lock:refresh, not just npm install.
An incremental npm install on Windows drops the wasm32-wasi optional packages
(@emnapi/*) from the lock while leaving @napi-rs/wasm-runtime's dependency
on them in place. Windows never installs those, so nothing looks wrong locally
β but on Linux npm ci resolves the range, finds no matching entry, and
refuses to install. It fails only in CI, at the very first step. lock:refresh
regenerates the lock from scratch and then verifies it with npm ci --dry-run.
The suite runs against a real PostgreSQL instance, not mocks β the schema, the audit trigger and
the JSONB queries are all genuinely exercised. tests/globalSetup.js creates a throwaway
microservice_panel_test database, applies db/schema.sql, and drops it afterwards. It refuses to
run at all unless the database name ends in _test, so a mistyped variable cannot touch real data.
What is covered: registration and login (including bcrypt hashing, timing-safe rejection of unknown emails, expired and forged tokens), permission enforcement per route, permissions granted and revoked mid-session on an already-issued token, pagination limits, the audit trigger, upload validation, and the error handler not leaking internals. Three tests specifically pin the registration route shut: an anonymous caller gets 401, a Student registering anyone gets 403, and a Student cannot mint a Supervisor β each asserting the row was never created, not merely that the status code was right.
Current line coverage is ~79%. The gap is rpc.routes.js and config/rabbitmq.js, which need a live
broker; only their failure paths are covered today.
- Docker and Docker Compose must be installed.
- Provide your Gemini key to the environment (optional, for the AI feature):
export GEMINI_API_KEY=your_key # Windows PowerShell: $env:GEMINI_API_KEY="your_key"
- Start all services:
docker-compose up --build
- Services:
- Frontend: http://localhost:3000
- Backend API: http://localhost:3006
- RabbitMQ panel: http://localhost:15672 (user/password:
guest/guest)
Backend:
# Prepare the .env file
cp .env.example .env # Windows: copy .env.example .env
# Fill in .env: DB details, GEMINI_API_KEY, and a JWT_SECRET.
# The server refuses to start without JWT_SECRET. Generate one with:
# node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"
# Set SEED_ADMIN_EMAIL / SEED_ADMIN_PASSWORD to create the first Supervisor.
npm install
npm run setup-db # applies db/schema.sql, seeds the first Supervisor
npm start # backend at http://localhost:3006Frontend:
cd frontend
cp .env.example .env.local # set NEXT_PUBLIC_API_URL if needed
npm install
npm run dev # http://localhost:3000You need to run PostgreSQL, RabbitMQ and (optionally) Redis separately for local development, or just start them from Docker:
docker-compose up -d postgres_db rabbitmq redis.Leaving
REDIS_URLblank is fine β the server says so at boot and reads permissions straight from PostgreSQL.
NEXT_PUBLIC_API_URLand the backend'sPORTare read independently and will not agree on their own β if you move the backend off 3006, change both.
npm run setup-db creates one Supervisor from SEED_ADMIN_EMAIL / SEED_ADMIN_PASSWORD; sign in
with those. Everyone else is created from that account, since /auth/register requires
users:write:
curl -X POST http://localhost:3006/auth/register \
-H "Authorization: Bearer <supervisor-token>" \
-H "Content-Type: application/json" \
-d '{"email":"student@example.com","password":"at-least-8-chars","role":"Student"}'Grab <supervisor-token> from the token field of POST /auth/login.
Secrets (GEMINI_API_KEY, DB password) are no longer hard-coded; they are read from .env, which is
not committed to git. See .env.example and frontend/.env.example for sample values.
db/schema.sqlis the source of truth for the schema, trigger and procedure.npm run setup-db(scripts/setup-db.js) applies it and then seeds the first Supervisor fromSEED_ADMIN_*. Every statement is idempotent, so re-running it never clobbers a Supervisor's runtime permission edits.db/seed.sqlis the original schema from the first version of the project, kept for reference only β do not run it. Its table names are still the Turkish ones used before the rename (genel_verilerβrecords,sistem_loglariβaudit_logs); the file header maps them all.
See CONTRIBUTORS.md.