ArchitectureTechnical Stack
Architecture

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

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

ConcernChoice
LanguageTypeScript (strict mode)
RuntimeNode.js
FrameworkExpress
API layerApollo Server (GraphQL)
ORMPrisma
DatabasePostgreSQL

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).

DetailValue
Tenant isolationPostgreSQL schema per organization
Schema naming pattern{client}_lotr_extr
Schemas on demo environment~70
Multi-DBNo — 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:

  • 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 edituser.org = resource.org AND user.tier = "premium" → can edit
Coarse-grainedFine-grained
Role assignmentAttribute 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

ConcernChoice
LanguageTypeScript
FrameworkReact (SPA)
GraphQL clientApollo Client
RoutingReact 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

EnvironmentWebAPIURL
DemoFirebase HostingDockerdemo.api.haroldwaste.com
StagingFirebase HostingDocker(internal)
ProductionFirebase HostingDocker(internal)

Deployment Targets

Web (harold-web):

  • Built as a static bundle and deployed to Firebase Hosting
  • Triggered from the staging branch (staging) and release branch (production)

API (harold-api):

  • Containerized with Docker
  • Deployed from the staging branch (staging) and release branch (production)

Branch Strategy

BranchPurposeDeploys to
feature/*All development workNothing
stagingPre-production validationStaging (Firebase + Docker)
releaseProduction releasesProduction (Firebase + Docker)
mainNever commit directlyNothing

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

RuleForbiddenRequired
Type safetyany, as any, as SomeTypeunknown + type guards
Null safety! (non-null assertion)Explicit guards or throw
Variable declarationsletconst only
Iterationfor loopsmap, filter, reduce, forEach
Object/array updatesDirect mutationSpread 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

LayerTechnologyNotes
Backend languageTypeScript + Node.jsStrict mode
Backend frameworkExpressThin HTTP layer
APIApollo Server (GraphQL)205 domain modules
ORMPrismaSchema + migrations
DatabasePostgreSQLSchema-per-tenant
AuthMagic.linkPasswordless, JWT
AuthorizationPermit.ioABAC
Feature flagsLaunchDarklyProgressive rollouts
FrontendReact SPATypeScript
GraphQL clientApollo ClientColocated queries
Web hostingFirebase HostingStatic bundle
API hostingDockerAll environments