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 devAccess Applications:
- Main app: http://localhost:3000
- API: http://localhost:3004
- Admin dashboard: http://localhost:5173
- Drizzle Studio: http://localhost:3005
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)
| App | Port | Description |
|---|---|---|
apps/app | 3000 | Main application (employee portal) |
apps/api | 3004 | Hono.js REST API backend |
apps/web | 3001 | Marketing website |
apps/consultant | 3008 | Consultant portfolio platform |
apps/council | 3009 | Local authority platform |
apps/charity | 3010 | Charity partner portal |
apps/docs | 3004 | Mintlify documentation |
apps/email | - | React Email templates |
apps/studio | 3005 | Drizzle Studio (DB UI) |
apps/storybook | 6006 | Component 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 fixWorking 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 buildDatabase 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:studioProduction 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 servicesArchitecture 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 THISCache 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.jsonImplementation 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 -- --coverage2. 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.tsTest 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, permissionscompany.ts- Company managementlicense.ts- License trackingconsultant.ts- Consultant featureskindness.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 fixGit Workflow
Branch Strategy
Main Branches:
main- Production-ready codedevelop- 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-matchingCommit Conventions
Format: type(scope): message
Types:
feat- New featurefix- Bug fixdocs- Documentation onlystyle- Code style changes (formatting)refactor- Code refactoringtest- Adding/updating testschore- 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 dependenciesPull Request Process
- Create PR with descriptive title
- Fill PR template (tests, breaking changes, screenshots)
- Request review from 1-2 team members
- Address feedback with additional commits
- Merge using squash and merge (clean history)
Common Development Tasks
Adding a New Evidence Module
Example: Creating a "Skills" module
- Create Package:
mkdir -p packages/skills
cd packages/skills- Create Files:
package.json- Package configurationtsconfig.json- TypeScript configindex.ts- Public APIqueries.ts- Database queriescache.ts- Redis cachingtypes.ts- TypeScript typesvalidation.ts- Zod schemas
- 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),
}),
);- Create API Routes:
apps/api/app/api/v1/skills/route.ts(POST, GET)apps/api/app/api/v1/skills/stats/route.ts(GET)
- Create UI Pages:
apps/app/app/(authenticated)/skills/page.tsx(list view)apps/app/app/(authenticated)/skills/new/page.tsx(form)
- 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 invalidSQL 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 checkRedis 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 buildDevelopment Tools
Drizzle Studio (Database UI):
pnpm db:studio
# Opens on http://localhost:3005Redis 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.ukHealth Checks:
- All apps expose
/api/healthendpoints - Database and Redis connectivity verified
- Strict mode enabled in production
Resources
Documentation
- Main Docs: https://docs.ripplecore.co.uk
- API Reference: https://docs.ripplecore.co.uk/api-reference
- PRD:
docs/content/docs/project/prd.md - Deployment Guide:
DEPLOYMENT.md - Setup Guide:
SETUP.md
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
- Next.js: https://nextjs.org/docs
- Drizzle ORM: https://orm.drizzle.team
- better-auth: https://better-auth.com
- Turborepo: https://turbo.build
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:
- ✅ Complete local setup
- ✅ Read PRD and architecture docs
- ✅ Pick up first issue from GitHub
- ✅ Attend team standup
- ✅ 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