How to Develop Locally
Prerequisites
| Tool | Version | Install |
|---|---|---|
| Node.js | >= 20.0.0 | nodejs.org |
| pnpm | repo-managed via Corepack | corepack enable && corepack prepare pnpm@11.5.3 --activate |
| Docker Compose | Latest | docker.com |
Quick Start
# First time - installs deps, sets up .env, builds shared packages:
pnpm setup
# Start infrastructure (Redis, PostgreSQL, Keycloak, MinIO):
docker compose up -d
# Start all services with hot reload:
pnpm dev
Open http://localhost:3000. The local host serves the public landing page and routes product app traffic through the web app and gateway.
Production domains:
luckyplans.xyz-> landing SPAdocs.luckyplans.xyz-> docs SPAbeta.luckyplans.xyz-> webapi.luckyplans.xyz-> api-gatewayadmin.luckyplans.xyz-> Keycloak and ArgoCDv0.api.luckyplans.xyz-> legacy API through thelegacy-v0-apiin-cluster proxy tohost.k3d.internal:9000
Step-by-Step Setup
1. Install dependencies
pnpm install
2. Set up environment variables
cp .env.example .env
Default values work for local development, including MinIO credentials (MINIO_ACCESS_KEY=minioadmin, MINIO_SECRET_KEY=minioadmin). You must fill in SESSION_SECRET and WORKER_CREDENTIAL_PEPPER:
openssl rand -base64 32
openssl rand -base64 48
Paste the outputs into SESSION_SECRET= and WORKER_CREDENTIAL_PEPPER= in .env.
3. Start infrastructure
Docker Compose provides only the local backing services needed for pnpm dev:
docker compose up -d
| Container | Port | Purpose |
|---|---|---|
| redis | 6379 | Inter-service communication and sessions |
| postgresql-keycloak | 5433 | Keycloak database |
| postgresql-app | 5434 | Application database |
| keycloak | 8080 | Identity provider |
| minio | 9000 (API), 9001 (Console) | Object storage for uploads |
Verify everything is healthy:
docker compose ps
Wait for Keycloak to show healthy status on first startup.
This compose file intentionally does not start the observability stack or any Kubernetes-only components. Those live under infrastructure/ and are exercised through the local Helm/k3d workflow instead.
To stop infrastructure:
docker compose down
docker compose down -v
4. Build shared packages and run database migrations
pnpm --filter @luckyplans/shared build
pnpm --filter @luckyplans/prisma build
pnpm --filter @luckyplans/prisma db:migrate:deploy
5. Start all services
pnpm dev
Common local entry points:
| Surface | URL | Description |
|---|---|---|
| Landing SPA | http://localhost:3000 | Public marketing site |
| Product app | http://localhost:3000/login | Authenticated web app |
| GraphQL | http://localhost:3000/graphql | Gateway endpoint via rewrite |
| Docs | http://localhost:3002 | Standalone docs SPA during pnpm dev |
6. Verify everything works
- Open http://localhost:3000 and confirm the landing page loads.
- Navigate to a protected route such as
/dashboard. - Confirm you are redirected to
/login. - Register a user or sign in with the test account:
- Email:
testuser@luckyplans.xyz - Password:
password
- Email:
- Confirm you are redirected back into the app with a valid session.
You can also verify the GraphQL endpoint directly:
curl http://localhost:3001/graphql -H 'Content-Type: application/json' \
-d '{"query":"{ health }"}'
7. MinIO console
Open http://localhost:9001:
- Username:
minioadmin - Password:
minioadmin
8. Keycloak admin console
Open http://localhost:8080:
- Admin user:
admin - Admin password:
admin - Realm:
luckyplans
The realm is imported from infrastructure/keycloak/realm-export.json on first start.
How Authentication Works Locally
The auth flow uses gateway-managed sessions and keeps tokens out of the browser:
- The browser requests a protected route.
- Next.js middleware checks for a
session_idcookie and redirects to/loginwhen absent. - The login page posts credentials to
/auth/login. - The gateway authenticates against Keycloak, creates a Redis session, and sets an HttpOnly
session_idcookie. - Subsequent GraphQL requests include that cookie automatically.
SessionGuardvalidates the session and injects the user into request context.
Registration follows the same pattern through /auth/register, with user creation handled server-side through the Keycloak Admin API.
Logging
Use the NestJS Logger from @nestjs/common for application logging:
import { Injectable, Logger } from '@nestjs/common';
@Injectable()
export class MyService {
private readonly logger = new Logger(MyService.name);
doSomething() {
this.logger.log('Processing request');
this.logger.warn('Cache miss, falling back');
this.logger.error('Failed to connect', error);
this.logger.debug('Detailed debug info');
}
}
Avoid console.log() for service-level application logs so logging stays consistent across the codebase.
Working on Specific Services
Run an individual surface when you do not need the full stack:
pnpm --filter @luckyplans/web dev
pnpm --filter @luckyplans/docs dev
pnpm --filter @luckyplans/api-gateway dev
pnpm --filter @luckyplans/service-core dev
pnpm --filter @luckyplans/edge-agent build
node apps/edge-agent/dist/main.js
Infrastructure still needs to be running through Docker Compose.
Running the Edge Agent Locally
First run
pnpm --filter @luckyplans/edge-agent build
node apps/edge-agent/dist/main.js
On first run the edge asks for:
- Edge display name
- Server URL
- Registration token
It generates a device number in this format:
edge-<human-name>-<shortid>
Non-interactive behavior
In headless environments, onboarding is blocked by default. Either provide valid runtime config ahead of time or opt in with EDGE_AGENT_ENABLE_ONBOARDING=1.
Runtime contract
The edge runtime:
- Registers with the gateway
- Sends connectivity heartbeats
- Polls and executes tasks
- Defers upgrades while work is active
- Runs upgrades only when idle
Running as an OS service
Build the edge agent before installing it as a service:
pnpm --filter @luckyplans/edge-agent build
Linux:
sudo pnpm --filter @luckyplans/edge-agent service:install
sudo pnpm --filter @luckyplans/edge-agent service:status
sudo pnpm --filter @luckyplans/edge-agent service:restart
sudo pnpm --filter @luckyplans/edge-agent service:uninstall
Windows:
pnpm --filter @luckyplans/edge-agent service:install
pnpm --filter @luckyplans/edge-agent service:status
pnpm --filter @luckyplans/edge-agent service:restart
pnpm --filter @luckyplans/edge-agent service:uninstall
Edge Lifecycle Operator Runbooks
Use these checks with gateway state and host service logs.
Stale worker
Symptoms:
- Worker
connectivityStatusisSTALEorOFFLINE lastSeenAtis not advancing- Edge host logs show no recent lifecycle activity
Checks:
- Confirm the host service is running.
- Check for
edge.daemon.started,edge.heartbeat.failed, andedge.daemon.iteration_failed. - Confirm
API_GATEWAY_URL,EDGE_WORKER_ID, andEDGE_WORKER_CREDENTIAL. - Confirm the gateway is reachable from the edge host.
- If the host recently upgraded, inspect the recovery state paths.
Target version stuck
Symptoms:
targetVersionis set butupgradeStatusremainsUPGRADE_PENDING- The worker keeps reporting
BUSY
Checks:
- Confirm the worker is idle.
- Check gateway logs for
edge.upgrade.target_assigned. - Check edge logs for
edge.upgrade.status_reported. - Confirm trusted upgrade environment variables are present.
- Inspect
EDGE_AGENT_UPGRADE_FAILED_TARGET_PATHif the same target failed before.
Verification failure
Symptoms:
upgradeStatusbecomesFAILED- Edge logs include
edge.upgrade.status_reported status=FAILED
Checks:
- Confirm the release artifact URL is reachable.
- Confirm metadata matches platform, arch, and install type.
- Confirm checksum and signature match the artifact.
- Confirm
EDGE_AGENT_UPGRADE_TRUSTED_PUBLIC_KEY_PEMmatches the signing key. - Do not paste private keys, worker credentials, or registration tokens into logs or issues.
Service restart failure
Symptoms:
upgradeStatusreachesRESTARTINGbut the service does not recover- Service manager logs show permission or startup errors
Checks:
- Confirm the service account can restart
luckyplans-edge-agent. - Inspect
systemctl statuson Linux orsc.exe queryon Windows. - Confirm the active version marker points at the target version.
- Follow rollback procedures before retrying the same target if startup failed after the marker switched.
Rollback
Symptoms:
upgradeStatusisROLLED_BACK- The failed target marker suppresses repeated installs for the same target version
Checks:
- Confirm the recovery state includes the previous version and attempt metadata.
- Confirm the active version marker was restored.
- Confirm the failed target marker records the rejected version.
- Check gateway logs for
edge.upgrade.status_transition. - Publish a corrected release or assign a different target version before retrying.
Working with GraphQL
Regenerate frontend types
After modifying a graphql() call in a hook or changing the schema:
pnpm --filter @luckyplans/web codegen
Schema location
Frontend codegen reads the schema from apps/api-gateway/schema.graphql.
When the gateway schema changes:
- Start the gateway or run
pnpm devso it regeneratesschema.graphql. - Run
pnpm --filter @luckyplans/web codegen. - Commit the updated schema and generated types.
Working with Prisma Migrations
Prisma schema lives at packages/prisma/prisma/schema.prisma.
Creating a migration
pnpm --filter @luckyplans/prisma db:migrate:dev -- --name describe_the_change
Migration safety rules
Prisma generates DDL but never backfills data. Do not add a required column to a populated table without a default or backfill strategy.
Safe pattern:
- Add the column as nullable or with a default.
- Backfill existing rows.
- Tighten the constraint in a follow-up migration.
Production migrations
Migrations run automatically via a Helm pre-upgrade Job using prisma migrate deploy, so validate migration SQL before merging.
Working with Shared Packages
Shared packages live in packages/ and are used as workspace dependencies.
@luckyplans/shared
Exports:
- Types such as
User,ServiceResponse,PaginatedResponse, and profile entities - Enums such as
CoreMessagePatternandProficiency - Utilities such as
generateId(),getRedisConfig(), andgetEnvVar()
import { CoreMessagePattern, ServiceResponse, getRedisConfig } from '@luckyplans/shared';
@luckyplans/config
Shared presets include:
tsconfig.nextjs.jsontsconfig.nestjs.jsontsconfig.base.jsoneslint-preset.js
Adding a New Microservice
1. Create the service directory
mkdir -p apps/service-<name>/src
2. Copy boilerplate from an existing service
Use service-core as the template.
3. Update package.json
Set the package name to @luckyplans/service-<name>.
4. Define message patterns
Add patterns in shared types:
export enum NewServiceMessagePattern {
GET_SOMETHING = 'new-service.getSomething',
CREATE_SOMETHING = 'new-service.createSomething',
}
5. Create the service files
src/main.tssrc/app.module.tssrc/<name>.controller.tssrc/<name>.service.ts
6. Register it in the API Gateway
Create apps/api-gateway/src/<name>/ with:
<name>.module.ts<name>.resolver.ts
Then import the module into apps/api-gateway/src/app.module.ts.
7. Add a Helm template
Add a Deployment template under infrastructure/helm/luckyplans/templates/service-<name>/deployment.yaml.
8. Install dependencies and test
pnpm install
pnpm build
pnpm dev
All Commands
| Command | Description |
|---|---|
pnpm setup | First-time project setup |
pnpm dev | Start all services with hot reload |
pnpm build | Build all packages and apps |
pnpm lint | Lint all packages |
pnpm type-check | Type-check all packages |
pnpm format | Format all files |
pnpm format:check | Check formatting |
pnpm clean | Remove build artifacts |
pnpm deploy:local | Full deploy to local k3d |
./infrastructure/scripts/deploy-local.sh landing | Rebuild and redeploy the landing SPA |
./infrastructure/scripts/deploy-local.sh web | Rebuild and redeploy the product web app |
./infrastructure/scripts/deploy-local.sh --helm-only | Helm upgrade only |
pnpm deploy:teardown | Destroy the local k3d cluster |
pnpm deploy:status | Show cluster, Helm release, and pod status |
Troubleshooting
Infrastructure not starting
docker compose ps
docker compose logs keycloak
Redis connection refused
docker compose up -d
docker compose exec redis redis-cli ping
Keycloak not ready
docker compose ps keycloak
curl -sf http://localhost:8080/realms/luckyplans
Login redirect loop
- Ensure the gateway is running.
- Ensure
SESSION_SECRETis set in.env. - Check browser cookies for
session_id.
Port already in use
# Windows
netstat -ano | findstr :3000
Turborepo cache issues
pnpm clean
pnpm install
pnpm build
Shared package changes not reflected
pnpm --filter @luckyplans/shared build
pnpm dev