RippleCore
Guides

Developer Onboarding Guide

Onboarding guide for new developers joining the RippleCore engineering team

Developer Onboarding Guide

Welcome to the RippleCore Engineering Team!

Overview

RippleCore is a social impact evidence platform built on a modern TypeScript monorepo stack. This guide will help you set up your development environment, understand the architecture, and start contributing.

Tech Stack: Next.js 16, React 19, PostgreSQL 18, Redis 7, Drizzle ORM, better-auth, Turborepo


Quick Start (5 Minutes)

Prerequisites

  • Node.js: v20.x LTS or higher
  • pnpm: v10.20.0 or higher
  • Docker: For PostgreSQL and Redis
  • Git: For version control

Installation

# Clone repository
git clone https://github.com/ripplecore/ripplecore-forge.git
cd ripplecore-forge

# Install dependencies
pnpm install

# Start infrastructure (PostgreSQL + Redis)
pnpm db:up

# Generate better-auth secret
npx @better-auth/cli secret

# Create environment files
cp .env.example apps/app/.env.local
cp .env.example apps/api/.env.local

# Edit apps/app/.env.local and apps/api/.env.local:
# - Add BETTER_AUTH_SECRET from above
# - Verify DATABASE_URL and REDIS_URL

# Push database schema
pnpm db:generate
pnpm db:push

# Start development
pnpm dev

Access Applications:


Architecture Overview

Modular Monolith Design

Philosophy (PRD Lines 331-333):

  • Modular monolith with microservice authentication
  • Clear service boundaries for future extraction
  • 121 services across 6 categories
  • Type-safe end-to-end development

System Layers

┌─────────────────────────────────────────┐
│          Frontend Layer                  │
│  - Next.js 16 App Router                │
│  - React 19 Server Components           │
│  - Vite Admin Dashboard                 │
└─────────────────┬───────────────────────┘

┌─────────────────▼───────────────────────┐
│        API Gateway Layer                │
│  - Hono.js Backend (port 3004)          │
│  - 190+ API routes                      │
│  - Rate limiting (Redis)                │
└─────────────────┬───────────────────────┘

┌─────────────────▼───────────────────────┐
│      Authentication Service             │
│  - better-auth (port 3002)              │
│  - Separate PostgreSQL database         │
│  - JWT tokens, OAuth, MFA               │
└─────────────────┬───────────────────────┘

┌─────────────────▼───────────────────────┐
│         Service Layer                   │
│  - 121 services in @repo/core           │
│  - Evidence modules (4)                 │
│  - Business logic (25)                  │
│  - Infrastructure (35)                  │
└─────────────────┬───────────────────────┘

┌─────────────────▼───────────────────────┐
│          Data Layer                     │
│  - PostgreSQL 18 (primary)              │
│  - Redis 7 (cache/sessions)             │
│  - Drizzle ORM (type-safe SQL)          │
└─────────────────────────────────────────┘

Monorepo Structure

Apps (11 deployable applications)

AppPortDescription
apps/app3000Main application (employee portal)
apps/api3004Hono.js REST API backend
apps/web3001Marketing website
apps/consultant3008Consultant portfolio platform
apps/council3009Local authority platform
apps/charity3010Charity partner portal
apps/docs3004Mintlify documentation
apps/email-React Email templates
apps/studio3005Drizzle Studio (DB UI)
apps/storybook6006Component library
apps/migrations-Database migration job

Packages (31 shared packages)

Infrastructure:

  • @repo/database - Drizzle ORM with 13+ tables
  • @repo/redis - Redis client with SCAN-based operations
  • @repo/auth - better-auth with organization plugin
  • @repo/design-system - RippleCore-branded UI components

Evidence Modules:

  • @repo/kindness - Kindness & recognition tracking
  • @repo/volunteer - Volunteer management
  • @repo/donation - Donation tracking
  • @repo/wellbeing - Wellbeing surveys

Business Logic:

  • @repo/tenant - Company/organization management
  • @repo/licensing - License usage tracking
  • @repo/consultant - Consultant portfolio (15 services)
  • @repo/council - Council integration (TOMs calculator)
  • @repo/charity - Charity partner management

Utilities: analytics, ai, email, notifications, observability, payments, storage, webhooks, etc.


Development Workflow

Daily Development

# Start infrastructure
pnpm db:up

# Start all apps in dev mode
pnpm dev

# Run tests
pnpm test

# Type check
pnpm check

# Lint and format
pnpm fix

Working on Specific Packages

# Run command in specific package
pnpm --filter @repo/database db:generate
pnpm --filter @repo/kindness test

# Add dependency to package
pnpm add <package> --filter @repo/kindness

# Build single package
pnpm --filter @repo/auth build

Database Workflow

Schema Changes:

# 1. Edit schema in packages/database/schema/
# 2. Generate migration SQL
pnpm db:generate

# 3. Review in packages/database/migrations/
# 4. Push to local database (development)
pnpm db:push

# 5. Verify in Drizzle Studio
pnpm db:studio

Production Migrations:

# 1. Generate migrations locally
pnpm db:generate

# 2. Commit migration files
git add packages/database/migrations/
git commit -m "feat: add new table"

# 3. Deploy migration job in Dokploy
# 4. Restart app services

Architecture Patterns

Multi-Tenant Isolation

Critical Rule: Every table MUST include companyId for tenant isolation.

// packages/database/schema/example.ts
export const example = pgTable(
  "example",
  {
    id: text("id")
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    companyId: text("company_id")
      .notNull()
      .references(() => companies.id, { onDelete: "cascade" }),
    // ... other fields
  },
  (table) => ({
    companyIdx: index("example_company_idx").on(table.companyId),
  }),
);

Redis Cache Patterns

Critical Rule: NEVER use redis.keys() in production.

import { deleteKeysByPattern, getKeysByPattern } from "@repo/redis";

// ✅ CORRECT: Non-blocking SCAN operation
await deleteKeysByPattern("company:123:*");

// ❌ WRONG: Blocks entire Redis instance
await redis.keys("company:*"); // NEVER DO THIS

Cache Key Naming:

import { CacheKeys, CacheTTL } from "@repo/redis";

// Session keys
CacheKeys.session(sessionId);

// Company-scoped
CacheKeys.company.kindness(companyId);

// User-scoped
CacheKeys.user.profile(userId);

// Set with TTL
await redis.setex(
  CacheKeys.company.kindness(companyId),
  CacheTTL.MEDIUM, // 5 minutes
  JSON.stringify(data),
);

Evidence Package Pattern

Standard Structure:

packages/[module]/
├── index.ts          # Public API with caching
├── queries.ts        # Drizzle queries
├── cache.ts          # Redis helpers
├── types.ts          # TypeScript types
├── validation.ts     # Zod schemas
└── package.json

Implementation Pattern:

// packages/evidence-name/index.ts
import "server-only";
import { database } from "@repo/database";
import { evidenceTable } from "@repo/database/schema/evidence";
import { redis, CacheKeys, CacheTTL } from "@repo/redis";

export async function createEvidence(data: CreateEvidenceInput) {
  // Validate with Zod
  const validated = createEvidenceSchema.parse(data);

  // Insert to database
  const [result] = await database
    .insert(evidenceTable)
    .values(validated)
    .returning();

  // Invalidate cache
  await invalidateCompanyCache(data.companyId);

  return result;
}

export async function listEvidence(companyId: string) {
  const cacheKey = CacheKeys.company.evidence(companyId);

  // Check cache
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);

  // Query database
  const results = await database.query.evidenceTable.findMany({
    where: eq(evidenceTable.companyId, companyId),
    orderBy: [desc(evidenceTable.createdAt)],
    limit: 100,
  });

  // Cache for 5 minutes
  await redis.setex(cacheKey, CacheTTL.MEDIUM, JSON.stringify(results));

  return results;
}

API Route Pattern

// apps/api/app/api/module/route.ts
import { auth } from "@repo/auth/server";
import { createEvidence, listEvidence } from "@repo/module";
import { headers } from "next/headers";
import { NextResponse } from "next/server";

export async function POST(request: Request) {
  // 1. Authenticate
  const session = await auth.api.getSession({
    headers: await headers(),
  });

  if (!session?.user) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  // 2. Get organization context
  const orgId = session.session.activeOrganizationId;

  if (!orgId) {
    return NextResponse.json(
      { error: "No active organization" },
      { status: 400 },
    );
  }

  // 3. Parse and validate
  const body = await request.json();

  // 4. Call package function (validation happens in package)
  const result = await createEvidence({
    ...body,
    companyId: orgId,
    userId: session.user.id,
  });

  // 5. Return response
  return NextResponse.json({ data: result }, { status: 201 });
}

Testing Strategy

Three-Tier Testing

1. Unit Tests (Vitest)

# Run all unit tests
pnpm test

# Run specific package tests
pnpm --filter @repo/kindness test

# Watch mode
pnpm --filter @repo/kindness test --watch

# Coverage
pnpm test -- --coverage

2. Integration Tests (TestContainers)

// Uses real PostgreSQL and Redis instances
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { PostgreSqlContainer } from "@testcontainers/postgresql";
import { createKindness } from "@repo/kindness";

describe("Kindness Integration Tests", () => {
  let container;

  beforeAll(async () => {
    container = await new PostgreSqlContainer().start();
    // Configure database connection
  });

  afterAll(async () => {
    await container.stop();
  });

  it("creates kindness with cache invalidation", async () => {
    // Test with real database
  });
});

3. E2E Tests (Playwright)

# Run E2E tests
pnpm --filter app test:e2e

# Run with UI
pnpm --filter app test:e2e:ui

# Run specific test
pnpm --filter app test:e2e -- admin-portal.spec.ts

Test Coverage Targets

  • Unit Tests: >95% (current: 87%)
  • Integration Tests: >70% critical paths
  • E2E Tests: 100% critical user journeys
  • Type Safety: 100% (strict mode enforced)

Code Quality

TypeScript Strict Mode

Configuration (non-negotiable):

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  }
}

Type Organization: All types in packages/core/src/types/ with domain separation:

  • user.ts - User, roles, permissions
  • company.ts - Company management
  • license.ts - License tracking
  • consultant.ts - Consultant features
  • kindness.ts, volunteer.ts, donation.ts, wellbeing.ts - Evidence modules

Validation with Zod

Required at API boundaries:

import { z } from "zod";

// Define schema
const createKindnessSchema = z.object({
  description: z.string().min(1).max(500),
  recipientId: z.string().uuid(),
  companyId: z.string().uuid(),
});

// Validate at API route
export async function POST(request: Request) {
  const body = await request.json();
  const validated = createKindnessSchema.parse(body); // Throws on invalid

  // TypeScript knows validated is correctly typed
  const result = await kindnessService.create(validated);
  return success(result);
}

Linting & Formatting

Biome (replaces ESLint + Prettier):

# Check code quality
pnpm check

# Auto-fix issues
pnpm fix

Git Workflow

Branch Strategy

Main Branches:

  • main - Production-ready code
  • develop - Integration branch (if used)

Feature Branches:

# Create feature branch
git checkout -b feature/add-volunteer-matching

# Work on feature
git add .
git commit -m "feat: implement skills-based volunteer matching"

# Push and create PR
git push -u origin feature/add-volunteer-matching

Commit Conventions

Format: type(scope): message

Types:

  • feat - New feature
  • fix - Bug fix
  • docs - Documentation only
  • style - Code style changes (formatting)
  • refactor - Code refactoring
  • test - Adding/updating tests
  • chore - Maintenance tasks

Examples:

feat(kindness): add AI sentiment analysis
fix(auth): resolve session timeout issue
docs(api): update OpenAPI specification
refactor(volunteer): optimize matching algorithm
test(donation): add integration tests
chore(deps): update dependencies

Pull Request Process

  1. Create PR with descriptive title
  2. Fill PR template (tests, breaking changes, screenshots)
  3. Request review from 1-2 team members
  4. Address feedback with additional commits
  5. Merge using squash and merge (clean history)

Common Development Tasks

Adding a New Evidence Module

Example: Creating a "Skills" module

  1. Create Package:
mkdir -p packages/skills
cd packages/skills
  1. Create Files:
  • package.json - Package configuration
  • tsconfig.json - TypeScript config
  • index.ts - Public API
  • queries.ts - Database queries
  • cache.ts - Redis caching
  • types.ts - TypeScript types
  • validation.ts - Zod schemas
  1. Create Database Schema:
// packages/database/schema/skills.ts
export const skills = pgTable(
  "skills",
  {
    id: text("id")
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    companyId: text("company_id")
      .notNull()
      .references(() => companies.id, { onDelete: "cascade" }),
    userId: text("user_id").notNull(),
    skillName: text("skill_name").notNull(),
    proficiencyLevel: integer("proficiency_level").notNull(), // 1-5
    verifiedBy: text("verified_by"),
    createdAt: timestamp("created_at").notNull().defaultNow(),
  },
  (table) => ({
    companyIdx: index("skills_company_idx").on(table.companyId),
    userIdx: index("skills_user_idx").on(table.userId),
  }),
);
  1. Create API Routes:
  • apps/api/app/api/v1/skills/route.ts (POST, GET)
  • apps/api/app/api/v1/skills/stats/route.ts (GET)
  1. Create UI Pages:
  • apps/app/app/(authenticated)/skills/page.tsx (list view)
  • apps/app/app/(authenticated)/skills/new/page.tsx (form)
  1. Add Tests:
  • Unit tests for business logic
  • Integration tests with TestContainers
  • E2E tests for critical paths

Adding an API Endpoint

Example: GET /api/skills/trending

// apps/api/app/api/v1/skills/trending/route.ts
import { auth } from "@repo/auth/server";
import { getTrendingSkills } from "@repo/skills";
import { headers } from "next/headers";
import { NextResponse } from "next/server";

export async function GET(request: Request) {
  const session = await auth.api.getSession({
    headers: await headers(),
  });

  if (!session?.user) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const orgId = session.session.activeOrganizationId;

  const skills = await getTrendingSkills({
    companyId: orgId,
    limit: 10,
  });

  return NextResponse.json({ data: skills });
}

Adding a UI Component

Example: SkillBadge Component

// apps/app/components/skills/skill-badge.tsx
"use client";

import { Badge } from "@repo/design-system/components/ui/badge";

interface SkillBadgeProps {
  skillName: string;
  proficiencyLevel: 1 | 2 | 3 | 4 | 5;
  verified?: boolean;
}

export function SkillBadge({
  skillName,
  proficiencyLevel,
  verified
}: SkillBadgeProps) {
  const color = proficiencyLevel >= 4 ? "default" : "secondary";

  return (
    <Badge variant={color}>
      {skillName} ({proficiencyLevel}/5)
      {verified && " ✓"}
    </Badge>
  );
}

Performance Optimization

Response Time Targets

PRD Requirements (Lines 840-846):

  • API response: <200ms
  • Page load: <2 seconds
  • Interaction: <100ms
  • Bundle size: <200KB

Optimization Checklist

API Performance:

  • ✅ Redis caching for frequently-read data
  • ✅ Database query optimization (indexes)
  • ✅ Connection pooling
  • ✅ Rate limiting to prevent abuse

Frontend Performance:

  • ✅ Server Components by default
  • ✅ Dynamic imports for large components
  • ✅ Image optimization (Next.js Image)
  • ✅ Font optimization (local fonts)

Bundle Optimization:

# Analyze bundle size
pnpm analyze

# Check for large dependencies
pnpm why <package-name>

Security Best Practices

Authentication

Getting Session (Server Components):

import { auth } from "@repo/auth/server";
import { headers } from "next/headers";

const session = await auth.api.getSession({
  headers: await headers(),
});

if (!session?.user) {
  redirect("/sign-in");
}

const companyId = session.session.activeOrganizationId;
const userId = session.user.id;

Admin Routes:

import { requireAdminSession } from "@repo/auth/helpers";

export default async function AdminPage() {
  const { session } = await requireAdminSession();
  // Admin-only page content
}

Input Validation

Always validate at API boundaries:

// packages/module/validation.ts
export const createSchema = z.object({
  field: z.string().min(1).max(500),
});

// apps/api/app/api/module/route.ts
const body = await request.json();
const validated = createSchema.parse(body); // Throws ZodError if invalid

SQL Injection Prevention

Use Drizzle ORM (parameterized queries):

// ✅ CORRECT: Parameterized query
const results = await database.query.users.findMany({
  where: eq(users.email, userEmail),
});

// ❌ WRONG: Raw SQL with interpolation
await database.execute(sql`SELECT * FROM users WHERE email = ${userEmail}`);

Debugging

Common Issues

Database Connection Failed:

# Check Docker containers
docker ps | grep -E "postgres|redis"

# Restart infrastructure
pnpm db:down && pnpm db:up

# Test connection
docker exec -it ripplecore-postgres psql -U ripplecore -d ripplecore -c "SELECT 1"

Type Errors After Schema Changes:

# Regenerate types
pnpm db:generate

# Type check all packages
pnpm check

Redis Cache Issues:

# Clear cache (development only)
docker exec -it ripplecore-redis redis-cli -a ripplecore_dev FLUSHDB

# View keys (development only)
docker exec -it ripplecore-redis redis-cli -a ripplecore_dev --scan --pattern "company:*"

Build Failures:

# Clean and rebuild
pnpm clean
rm -rf node_modules pnpm-lock.yaml
pnpm install
pnpm build

Development Tools

Drizzle Studio (Database UI):

pnpm db:studio
# Opens on http://localhost:3005

Redis Commander (Cache UI):

# Access at http://localhost:8081 (when running docker-compose.dev.yml)

React DevTools: Browser extension for debugging React components

Network Tab: Monitor API calls and performance


Deployment

Production Deployment

Platform: Dokploy on Hetzner VPS

Complete Guide: See DEPLOYMENT.md for comprehensive instructions.

Quick Reference:

# 1. Deploy PostgreSQL + Redis in Dokploy
# 2. Deploy migration job (one-time)
# 3. Deploy apps: app → api → web → docs
# 4. Verify deployment
./scripts/verify-deployment.sh --domain ripplecore.co.uk

Health Checks:

  • All apps expose /api/health endpoints
  • Database and Redis connectivity verified
  • Strict mode enabled in production

Resources

Documentation

Internal Guides

  • CLAUDE.md: Development patterns and guidelines
  • ADMIN_GUIDE.md: Admin portal user manual
  • ROADMAP.md: Short and long-term plans
  • STRATEGIC_ENHANCEMENTS.md: Current implementation status

External Resources


Team Communication

Channels

  • Slack: #engineering (general), #frontend, #backend, #infrastructure
  • GitHub: Issues and pull requests
  • Email: engineering@ripplecore.co.uk
  • Standup: Daily at 9:30 AM UK time

Code Review

Expectations:

  • All PRs require 1-2 approvals
  • Response within 24 hours
  • Constructive feedback
  • Focus on learning and improvement

Reviewing:

  • Check for test coverage
  • Verify type safety
  • Review for security issues
  • Test locally if significant changes

Welcome to RippleCore!

You're now part of a team building the future of social impact measurement. We're excited to have you aboard!

Next Steps:

  1. ✅ Complete local setup
  2. ✅ Read PRD and architecture docs
  3. ✅ Pick up first issue from GitHub
  4. ✅ Attend team standup
  5. ✅ Deploy your first PR!

Questions? Ask in #engineering or email engineering@ripplecore.co.uk


Document Version: 1.0 Last Updated: November 15, 2025 For: RippleCore Engineering Team