Technical Stack
Comprehensive technical reference for the Jules (Harold) ERP — backend, frontend, infrastructure, architecture decisions, and development conventions for onboarding developers.
Technical Stack
Jules (internal codename: Harold) is a vertical SaaS ERP built for recyclable commodity trading. This document is the authoritative technical reference for engineers joining the project.
Table of Contents
- Repository Structure
- Backend — harold-api
- Frontend — harold-web
- Infrastructure
- Architecture Decisions
- Development Conventions
- Local Development
Repository Structure
jules/
├── harold-api/ # Backend — Node.js / TypeScript
│ ├── src/
│ │ └── api/ # 205 GraphQL domain modules
│ ├── prisma/
│ │ ├── schema.prisma # Prisma schema
│ │ └── migrations/ # Migration history
│ └── ...
└── harold-web/ # Frontend — React SPA
└── src/
└── ...
The harold-api/src/api/ directory contains 205 GraphQL modules, each scoped to a single business domain (e.g., operations, shipments, allocations, invoicing). Each module owns its resolvers, types, and any domain-specific utilities.
Backend — harold-api
Language and Runtime
| Concern | Choice |
|---|---|
| Language | TypeScript (strict mode) |
| Runtime | Node.js |
| Framework | Express |
| API layer | Apollo Server (GraphQL) |
| ORM | Prisma |
| Database | PostgreSQL |
GraphQL — Apollo Server
The API layer is exclusively GraphQL. There is no REST layer for business logic. Apollo Server runs on top of Express and exposes a single /graphql endpoint.
The src/api/ directory is organized as one folder per domain:
src/api/
├── operations/
│ ├── operations.resolver.ts
│ ├── operations.schema.ts
│ └── operations.service.ts
├── shipments/
├── allocations/
├── invoicing/
└── ... # 205 modules total
Each domain module typically contains:
- schema — GraphQL type definitions (SDL)
- resolver — Query/Mutation handlers
- service — Business logic, Prisma calls
ORM — Prisma
Prisma is used for all database access. The schema lives at harold-api/prisma/schema.prisma.
Key Prisma conventions:
- Migrations are the source of truth for schema changes — never alter the database directly
- All new features require a migration file before merging
- The workflow is always: Prisma schema change → migration → resolver update → frontend
Database — PostgreSQL (Multi-Tenant)
Jules uses a single PostgreSQL instance with one schema per organization (multi-tenancy via schemas, not separate databases).
| Detail | Value |
|---|---|
| Tenant isolation | PostgreSQL schema per organization |
| Schema naming pattern | {client}_lotr_extr |
| Schemas on demo environment | ~70 |
| Multi-DB | No — single cluster, schema-level isolation |
The _lotr_extr suffix is a legacy naming convention inherited from the project's earliest internal naming ("LOTR" = internal project codename). It must be preserved for compatibility.
Authentication & Authorization
Jules uses three distinct systems for identity and access control:
Magic.link — Passwordless Authentication
- No passwords are stored anywhere in the system
- Users authenticate via a magic link sent to their email
- Magic.link issues a JWT that is validated on each API request
- This removes password management, resets, and credential storage entirely
Permit.io — Authorization (ABAC)
- Attribute-Based Access Control (ABAC), not RBAC
- Permissions are evaluated against attributes: user attributes, resource attributes, and environment context
- Permit.io is the single source of truth for what a user can do
- Authorization checks happen inside resolvers before any data is returned
ABAC vs RBAC in this context:
| RBAC (not used) | ABAC (used) |
|---|---|
role = admin → can edit | user.org = resource.org AND user.tier = "premium" → can edit |
| Coarse-grained | Fine-grained |
| Role assignment | Attribute evaluation |
LaunchDarkly — Feature Flags
- Used to gate features behind progressive rollouts
- Allows enabling a feature for a specific tenant, user group, or percentage of traffic without a deployment
- Feature flag checks happen at the resolver level
Frontend — harold-web
| Concern | Choice |
|---|---|
| Language | TypeScript |
| Framework | React (SPA) |
| GraphQL client | Apollo Client |
| Routing | React Router |
harold-web is a Single Page Application. All data fetching goes through Apollo Client, which communicates exclusively with the GraphQL endpoint on harold-api.
There is no direct database access or REST API consumption in the frontend.
Infrastructure
Environments
| Environment | Web | API | URL |
|---|---|---|---|
| Demo | Firebase Hosting | Docker | demo.api.haroldwaste.com |
| Staging | Firebase Hosting | Docker | (internal) |
| Production | Firebase Hosting | Docker | (internal) |
Deployment Targets
Web (harold-web):
- Built as a static bundle and deployed to Firebase Hosting
- Triggered from the
stagingbranch (staging) andreleasebranch (production)
API (harold-api):
- Containerized with Docker
- Deployed from the
stagingbranch (staging) andreleasebranch (production)
Branch Strategy
| Branch | Purpose | Deploys to |
|---|---|---|
feature/* | All development work | Nothing |
staging | Pre-production validation | Staging (Firebase + Docker) |
release | Production releases | Production (Firebase + Docker) |
main | Never commit directly | Nothing |
Architecture Decisions
Multi-Tenancy via PostgreSQL Schemas
Decision: One schema per tenant in a single PostgreSQL cluster, not separate databases or row-level tenancy.
Why:
- Strong tenant isolation without the operational overhead of multiple databases
- Schema-level permissions and queries keep tenant data separate
- Prisma handles schema-switching at connection time
Tradeoff: Schema proliferation at scale (currently ~70 on demo). Migrations must run against each schema individually.
GraphQL-Only API
Decision: No REST endpoints for business logic. Apollo GraphQL is the only external API surface.
Why:
- Eliminates over-fetching and under-fetching in a complex domain with many related entities
- 205 domain modules map cleanly to GraphQL types and resolvers
- Apollo Client on the frontend can cache, batch, and colocate queries with components
Tradeoff: GraphQL schema governance is critical — breaking changes must be handled carefully.
ABAC over RBAC
Decision: Attribute-Based Access Control via Permit.io instead of simple role-based rules.
Why:
- The trading ERP domain has complex, contextual permissions (e.g., a user can edit their own org's contracts but not another org's, and only if the contract status is "draft")
- ABAC expresses these rules without combinatorial role explosion
- Permit.io externalizes authorization logic out of resolver code
Passwordless Auth
Decision: Magic.link for authentication — no passwords stored.
Why:
- Eliminates an entire class of security vulnerabilities (credential stuffing, password breaches)
- Reduces auth-related support burden (no "forgot password" flows to maintain)
- B2B SaaS users are already accustomed to email-based access
Development Conventions
These rules are enforced across the entire codebase. PRs that violate them will not be merged.
TypeScript Rules
| Rule | Forbidden | Required |
|---|---|---|
| Type safety | any, as any, as SomeType | unknown + type guards |
| Null safety | ! (non-null assertion) | Explicit guards or throw |
| Variable declarations | let | const only |
| Iteration | for loops | map, filter, reduce, forEach |
| Object/array updates | Direct mutation | Spread copies |
Examples:
// WRONG
const result = someFunction() as MyType;
const value = obj!.field;
let count = 0;
for (let i = 0; i < items.length; i++) { ... }
arr.push(newItem);
// CORRECT
const result = someFunction();
if (!isMyType(result)) throw new Error("Unexpected type");
const value = obj?.field ?? throw new Error("obj is null");
const count = 0;
const processed = items.map((item) => transform(item));
const newArr = [...arr, newItem];
Feature Development Workflow
New features always follow this order:
1. harold-api → Prisma schema change
2. harold-api → Migration (prisma migrate dev)
3. harold-api → GraphQL schema (SDL types)
4. harold-api → Resolver + Service implementation
5. harold-web → React component + Apollo queries/mutations
Never start frontend work before the API contract is defined.
Branch Naming
feature/short-description-of-work
Never commit directly to main, staging, or release.
Pre-Merge Checks
Before opening a PR, verify:
yarn lint
yarn type:check
Both must pass with zero errors. No exceptions.
Local Development
Detailed setup instructions are maintained in the repository README. This section covers the key points relevant to the architecture.
Environment Variables
Each environment requires its own .env file. Key variables include:
- Database connection string (PostgreSQL, pointing to the target schema)
- Magic.link publishable and secret keys
- Permit.io API key
- LaunchDarkly SDK key
Database Access
The MCP database tool connected to the demo environment is read-only. Never run INSERT, UPDATE, DELETE, or DDL statements against any environment outside of a proper migration workflow.
Running Migrations
# In harold-api/
npx prisma migrate dev --name describe-your-change
This generates a migration file and applies it to your local database. Migration files are committed to the repository and applied to staging/production as part of the deployment pipeline.
Quick Reference
| Layer | Technology | Notes |
|---|---|---|
| Backend language | TypeScript + Node.js | Strict mode |
| Backend framework | Express | Thin HTTP layer |
| API | Apollo Server (GraphQL) | 205 domain modules |
| ORM | Prisma | Schema + migrations |
| Database | PostgreSQL | Schema-per-tenant |
| Auth | Magic.link | Passwordless, JWT |
| Authorization | Permit.io | ABAC |
| Feature flags | LaunchDarkly | Progressive rollouts |
| Frontend | React SPA | TypeScript |
| GraphQL client | Apollo Client | Colocated queries |
| Web hosting | Firebase Hosting | Static bundle |
| API hosting | Docker | All environments |
Last updated today
Built with Documentation.AI