QAtrial v3.0.0 is a full-stack quality management platform built with React 19, Hono, PostgreSQL, and Prisma ORM. This article is a technical walkthrough of the architecture for developers, architects, and technical evaluators considering QAtrial for regulated environments.
The codebase comprises 130+ TypeScript source files and 25,000+ lines of code. It supports 12 languages, 10 industry verticals, and 37 countries. Everything runs from a single repository with dual deployment modes: standalone (browser-only with localStorage) and server (full PostgreSQL backend).
Architecture Overview
Browser (React 19) --> Hono Server (port 3001) --> PostgreSQL 16
| | |
|-- Vite dev server |-- 60+ API endpoints |-- 15 Prisma models
|-- 14 code-split chunks |-- 21 route files |-- Append-only audit log
|-- 20 Zustand stores |-- JWT auth middleware |-- ACID transactions
|-- TanStack Table v8 |-- RBAC permission checks |-- Referential integrity
|-- react-i18next |-- Webhook dispatch |
|-- Recharts |-- AI proxy |
|-- SSO (OIDC) |
|-- Static file serving |
The frontend communicates with the backend exclusively through REST API calls authenticated with Bearer tokens. An apiClient.ts wrapper injects the JWT access token into every request and handles token refresh transparently.
Architecture
docker-compose up.| Model | Category | Purpose | Key Fields |
|---|---|---|---|
| User | Auth | Authentication and identity | email · passwordHash · role · orgId |
| Organization | Auth | Multi-tenant isolation | name · slug |
| Project | Quality | Quality project container | name · country · vertical · projectType · modules[] · orgId |
| Requirement | Quality | Quality requirement | seqId (REQ-NNN) · title · description · status · riskLevel · regulatoryRefs[] · tags[] |
| Test | Quality | Test case | seqId (TST-NNN) · title · status · linkedRequirements[] |
| CAPA | Quality | Corrective/preventive action | title · status (6-state) · rootCause · correctiveAction |
| Risk | Quality | Risk assessment | severity · likelihood · detectability · riskLevel (auto-calculated) |
| Evidence | Quality | File attachment | filename · path · entityType · entityId |
| Approval | Quality | Approval workflow record | entityType · entityId · status (pending/approved/rejected) · reviewerId |
| Signature | Audit | Electronic signature (21 CFR Part 11) | userId · meaning · reason · timestamp · passwordVerified |
| AuditLog | Audit | Append-only event log | userId · action (16 types) · entityType · entityId · previousValue · newValue |
| Webhook | Integration | Webhook configuration | url · secret · events[] · enabled · lastTriggered · lastStatus |
| Integration | Integration | External connector config | type (jira/github) · config (JSON) · enabled · lastSyncAt |
| Session | Auth | Refresh token management | userId · refreshToken · expiresAt |
| Setting | System | System configuration | key · value · orgId |
In standalone mode, the same React components write to Zustand stores backed by localStorage. This allows the frontend to function as a fully operational demo without any server infrastructure. The import/export system, AI panel, and setup wizard all detect the current mode and route operations accordingly.

The Road to React: The React.js 19 with Hooks in JavaScript Book (2025 Edition)
As an affiliate, we earn on qualifying purchases.
As an affiliate, we earn on qualifying purchases.
Database Layer: 15 Prisma Models
The PostgreSQL schema is defined in server/prisma/schema.prisma using Prisma ORM v7. The 15 models cover every entity in the quality management domain:
| Model | Purpose | Key Fields |
|---|---|---|
| User | Authentication and identity | email, passwordHash, role, orgId |
| Organization | Multi-tenant isolation | name, slug |
| Project | Quality project container | name, country, vertical, projectType, modules[], orgId |
| Requirement | Quality requirement | seqId (REQ-NNN), title, description, status, riskLevel, regulatoryRefs[], tags[] |
| Test | Test case | seqId (TST-NNN), title, description, status, linkedRequirements[] |
| CAPA | Corrective/preventive action | title, description, status (6-state lifecycle), rootCause, correctiveAction |
| Risk | Risk assessment | severity, likelihood, detectability, riskLevel (auto-calculated) |
| Evidence | File attachment | filename, path, entityType, entityId |
| Approval | Approval workflow record | entityType, entityId, status (pending/approved/rejected), reviewerId |
| Signature | Electronic signature | userId, meaning, reason, timestamp, passwordVerified |
| AuditLog | Append-only event log | userId, action (16 types), entityType, entityId, previousValue, newValue, reason |
| Webhook | Webhook configuration | url, secret, events[], enabled, lastTriggered, lastStatus |
| Integration | External connector config | type (jira/github), config (JSON), enabled, lastSyncAt |
| Session | Token management | userId, refreshToken, expiresAt |
| Setting | System configuration | key, value, orgId |
Sequential IDs (REQ-001, TST-001) are auto-generated per project by querying the maximum existing sequence number and incrementing. This runs inside a transaction to prevent race conditions in multi-user environments.

Learn PostgreSQL: Use, manage, and build secure and scalable databases with PostgreSQL 16
As an affiliate, we earn on qualifying purchases.
As an affiliate, we earn on qualifying purchases.
Authentication: JWT with Refresh Tokens
The auth system uses a dual-token approach:
- Access token (24-hour expiry): Included as a Bearer token in every API request. Contains the user’s ID, email, role, and organization ID. Validated by the
requireAuthmiddleware on every protected endpoint. - Refresh token (7-day expiry): Stored client-side and used to obtain a new access token when the current one expires. The
POST /api/auth/refreshendpoint validates the refresh token against the Session table and issues a new token pair.
Password hashing uses bcrypt with 12 rounds. The auth routes (server/routes/auth.ts) handle:
POST /api/auth/register— Create user account, hash password, assign default role, return token pair.POST /api/auth/login— Validate credentials, return token pair.POST /api/auth/refresh— Exchange refresh token for new token pair.GET /api/auth/me— Return current user profile from JWT claims.
The requirePermission(permission) middleware chains after requireAuth and checks the user’s role against the permission matrix:
admin: canView, canEdit, canApprove, canAdmin
qa_manager: canView, canEdit, canApprove
qa_engineer: canView, canEdit
auditor: canView
reviewer: canView, canApprove
Every mutation endpoint calls requirePermission('canEdit'). Approval endpoints call requirePermission('canApprove'). Administration endpoints call requirePermission('canAdmin'). View endpoints call requirePermission('canView').

TypeScript Programming Language – Software Engineer & Coder Pullover Hoodie
- Language Type: Superset of JavaScript with static typing
- Ideal Users: Front-end, full-stack developers, architects
- Application Scope: Builds large-scale web applications
As an affiliate, we earn on qualifying purchases.
As an affiliate, we earn on qualifying purchases.
Template Composition System
QAtrial’s template system generates project-specific requirements and tests based on the combination of country, vertical, project type, and modules. The composition follows a layered architecture:
Country (jurisdiction) x Vertical (domain) x Project Type x Modules
Registry (src/templates/registry.ts): Central registry that indexes all available countries, verticals, modules, and project types.
Composer (src/templates/composer.ts): Takes the user’s selections from the setup wizard and composes the final set of requirements and tests. The composition process:
- Load the base requirements for the selected country (regulatory authority, submission formats, local standards).
- Overlay vertical-specific requirements (e.g., pharma adds ICH Q7/Q10, medical devices adds ISO 13485/ISO 14971).
- Add module-specific requirements for each selected quality module.
- Filter by project type (validation projects get different templates than quality system projects).
- Generate test cases linked to the composed requirements.
Lazy loading: Template data is loaded on demand. The 10 vertical definitions, 15 module definitions, and country overlays are not bundled into the initial page load. They are imported dynamically when the setup wizard reaches the relevant step.
Compliance Starter Packs (src/templates/packs/index.ts): Four pre-configured bundles that set country, vertical, project type, and modules simultaneously. These bypass the manual wizard steps for common regulatory frameworks:
fda_csv: US + Software/IT + Validation + 7 modules (Part 11, GAMP 5 focus)eu_mdr: DE + Medical Devices + Quality System + 9 modules (ISO 13485 focus)fda_gmp: US + Pharma + Quality System + 10 modules (21 CFR 210/211 focus)iso_gdpr: DE + Software/IT + Compliance + 7 modules (ISO 27001/GDPR focus)

API FRESHWATER MASTER TEST KIT 800-Test Freshwater Aquarium Water Master Test Kit, White, Single, Multi-colored
- Kit Contents: Includes 800 tests, solutions, color card, and tubes
- Water Quality Monitoring: Helps prevent harmful water issues
- Vital Parameter Testing: Measures pH, high pH, ammonia, nitrite, nitrate
As an affiliate, we earn on qualifying purchases.
As an affiliate, we earn on qualifying purchases.
AI System: 9 Prompts, Multi-Provider, Server-Side Proxy
The AI subsystem is architected for flexibility and security:
Provider abstraction (src/ai/provider.ts): A unified interface that supports Anthropic (Claude) and OpenAI-compatible APIs. The OpenAI-compatible mode works with OpenAI directly, OpenRouter (multi-model gateway), and Ollama (local inference). Provider configuration is stored per-organization.
9 prompt templates (src/ai/prompts/):
| Prompt | Input | Output |
|---|---|---|
generateTests | Requirement + context (country, vertical, standards, risk) | 4-6 test cases with steps and expected results |
riskClassification | Requirement text + vertical | Severity, likelihood, risk level with rationale |
gapAnalysis | Project requirements + target standard | Covered/partial/missing clauses with remediation suggestions |
capaSuggestion | Failed test details | Root cause analysis + corrective/preventive action proposals |
executiveBrief | Project state | One-page C-level compliance summary |
vsrReport | Project data | 7-section Validation Summary Report |
qmsrGap | Requirements | QMSR/ISO 13485 gap analysis (27 clauses) |
reqExtraction | Source document text | Extracted requirements with metadata |
qualityCheck | Requirement text | Issues: vagueness, untestability, ambiguity, incompleteness, duplicates, missing criteria |
JSON Schema validation (src/ai/validation.ts): Every AI response is validated against an expected JSON schema. If validation fails, the system attempts automatic repair (re-parsing, extracting JSON from markdown code blocks). If repair fails, a retry is triggered with a more explicit prompt.
Provenance tracking: Every AI generation records the model used, parameters, token count, timestamp, and the ID of the human reviewer who accepted or modified the output. This creates an audit trail for AI-generated content, addressing regulatory expectations around AI output review.
Server-side proxy (server/routes/ai.ts):
POST /api/ai/complete— Accepts a prompt and parameters, proxies the call to the configured LLM provider. API keys are read from server environment variables (AI_PROVIDER_KEY) and never sent to the client.GET /api/ai/providers— Returns provider configuration with keys redacted.POST /api/ai/providers/:id/test— Tests provider connectivity.
This architecture means the browser never holds an AI API key. For pharma and device companies with strict data governance policies, this is a prerequisite for AI adoption.
Webhook Dispatch
Webhook delivery is implemented as a fire-and-forget pattern in server/services/webhook.service.ts:
- A mutation occurs (e.g., requirement updated).
- The route handler calls the webhook service with the event type and payload.
- The service queries all enabled webhooks that subscribe to this event type.
- For each matching webhook, the service:
a. Serializes the payload as JSON.
b. Computes an HMAC-SHA256 signature using the webhook’s secret.
c. Sends an HTTP POST with the payload and signature header.
d. Records the delivery status (lastTriggered, lastStatus) in the Webhook model. - Delivery failures are logged but do not block the original operation.
The fire-and-forget pattern ensures that webhook delivery issues (slow receivers, network problems) do not impact the user’s experience or create data consistency issues.
SSO: OIDC Discovery + Authorization Code Flow
The SSO implementation (server/routes/sso.ts) follows the standard OIDC authorization code flow:
- Discovery: On startup (or first SSO request), QAtrial fetches the IdP’s
.well-known/openid-configurationto discover the authorization endpoint, token endpoint, and JWKS URI. - Authorization redirect: When a user clicks “Sign in with SSO,” the browser is redirected to the IdP’s authorization endpoint with the configured client ID and callback URL.
- Callback: The IdP redirects back to
/api/auth/sso/callbackwith an authorization code. - Token exchange: QAtrial exchanges the code for an ID token and access token using the client secret.
- Auto-provisioning: If no QAtrial user exists for the IdP’s email claim, a new user is created with the configured default role (
SSO_DEFAULT_ROLE). - JWT issuance: QAtrial issues its own JWT access/refresh token pair, and the user is logged in.
Supported IdPs: Okta, Azure AD/Entra ID, Auth0, Keycloak, Google Workspace. Any OIDC-compliant IdP should work with the standard configuration.
Docker: Multi-Stage Build
The Dockerfile uses a three-stage build:
Stage 1 — Frontend build: Node.js image, npm install, npm run build. Produces the dist/ directory with the compiled React application, code-split into 14 Vite chunks.
Stage 2 — Server build: Compiles the Hono server TypeScript. Generates the Prisma client.
Stage 3 — Runtime: Slim Node.js image. Copies only the compiled server, generated Prisma client, and built frontend assets. No dev dependencies, no source TypeScript.
The production server serves the dist/ directory as static files. API routes are served under /api/. This means the entire application — frontend, backend, and API — runs from a single process on a single port.
docker-compose.yml defines two services:
app: The QAtrial container, port 3001, depends ondb, environment variables from.env.db: PostgreSQL 16, health check withpg_isready, named volumeqatrial-db-data.
Health checks ensure the database is fully ready before the application attempts to connect.
Audit Trail: Append-Only PostgreSQL
The audit service (server/services/audit.service.ts) writes to the AuditLog table on every state-changing operation. The service is called from every route handler that creates, updates, deletes, approves, signs, or otherwise modifies data.
Each audit log entry captures:
| Field | Content |
|---|---|
userId | The authenticated user’s ID (from JWT) |
action | One of 16 action types: create, update, delete, approve, reject, sign, status_change, import, export, login, logout, link, unlink, upload, download, config_change |
entityType | requirement, test, capa, risk, evidence, approval, signature, webhook, integration, user, project |
entityId | The specific record’s ID |
previousValue | JSON snapshot of the record before the change (null for creates) |
newValue | JSON snapshot of the record after the change (null for deletes) |
reason | Optional reason provided by the user (required for certain operations) |
timestamp | Server-generated UTC timestamp |
ipAddress | Request origin IP |
The table is append-only by design. There is no update or delete endpoint for audit log entries. The GET /api/audit/:projectId endpoint provides read access with filtering and pagination. GET /api/audit/:projectId/export produces a CSV file for offline review.
Import/Export
CSV Import (server/routes/import.ts):
POST /api/import/preview: Accepts a CSV file upload. Auto-detects the delimiter by sampling the first rows (supports comma, semicolon, tab). Returns: detected columns, sample data rows, and suggested column mapping based on header name similarity to QAtrial fields.POST /api/import/execute: Creates requirements or tests from the mapped data. Supports duplicate handling strategies: skip (by title match), overwrite (update existing), or create new. Each imported record is audit-logged.
CSV Export (server/routes/export.ts):
GET /api/export/:projectId/csv?type=requirements|tests|all: Returns a UTF-8 BOM-encoded CSV file. The BOM ensures correct character rendering when the file is opened in Excel on Windows (a common pain point with CSV exports).
In standalone mode, the frontend generates CSV directly from Zustand stores without server involvement.
Code Splitting
Vite is configured with manual chunks in vite.config.ts to optimize the initial load:
14 chunks split the application by feature area:
- Core framework (React, React DOM)
- UI libraries (TanStack Table, Recharts, Lucide)
- State management (Zustand stores)
- Internationalization (react-i18next + locale data)
- AI system (prompts, provider, validation)
- Templates (verticals, modules, regions, packs)
- Dashboard components
- Wizard components
- Import/export
- Settings
- Audit components
- Report generation
- Connectors
- Shared utilities
Lazy-loaded routes ensure that a user who opens only the requirements table does not download the AI prompt templates, report generators, or settings components until they navigate to those features.
Dual Mode: Standalone + Server
The same React codebase supports two operational modes:
Standalone mode: All data stored in browser localStorage via Zustand stores with persistence middleware. The 20 Zustand stores cover: projects, requirements, tests, CAPAs, risks, evidence, approvals, signatures, audit trail, settings, AI configuration, import state, wizard state, dashboard state, notifications, theme, language, auth (mock), connectors, and webhooks.
Server mode: All data operations go through the REST API. The apiClient.ts wrapper handles authentication, token refresh, and error handling. Zustand stores are still used for client-side state (current selection, UI state, cached data) but are not the source of truth.
Mode detection is automatic. If the frontend detects a configured VITE_API_URL and can reach the /api/status endpoint, it operates in server mode. Otherwise, it falls back to standalone mode.
This dual-mode architecture enables:
- Zero-setup demos and evaluations (standalone)
- Production deployment with full persistence, multi-user support, and audit compliance (server)
- Development without running the backend (standalone)
- Gradual migration from evaluation to production without data restructuring
Build and Development
| Command | Description |
|---|---|
npm run dev | Vite dev server (frontend only, port 5173) |
npm run build | Production build (frontend) |
npm run server:dev | Hono server with file watching (port 3001) |
npm run server | Hono server without watching |
npm run db:generate | Generate Prisma client from schema |
npm run db:migrate | Run Prisma migrations |
npm run db:push | Push schema directly to database |
npm run db:studio | Open Prisma Studio GUI |
docker-compose up | Start full stack (app + PostgreSQL) |
Codebase Statistics
| Metric | Value |
|---|---|
| Source files | 130+ |
| Lines of code | 25,000+ |
| Languages (i18n) | 12 (425+ translation keys each) |
| Industry verticals | 10 |
| Countries | 37 |
| Database models | 15 |
| API endpoints | 60+ |
| Route files | 21 |
| AI prompt templates | 9 |
| Zustand stores | 20 |
| Vite chunks | 14 |
| Quality modules | 15 |
| Compliance packs | 4 |
| Validation documents | 5 |
| Webhook events | 14 |
| Audit action types | 16 |
| RBAC roles | 5 |
Conclusion
QAtrial v3.0.0 is a complete quality management system, not a prototype or proof of concept. The architecture — React 19 frontend, Hono backend, PostgreSQL database, Prisma ORM, JWT authentication, five-role RBAC, append-only audit trail, OIDC SSO, webhook dispatch, and Docker deployment — addresses the technical requirements of regulated environments while remaining accessible to a single developer with docker-compose up.
The source code is available under AGPL-3.0 at https://github.com/MeyerThorsten/QAtrial.