This file provides guidance for AI coding agents working in this repository.
leadminer/
├── backend/ # Node.js/Express API (TypeScript)
├── frontend/ # Nuxt.js Vue frontend (TypeScript)
├── supabase/functions/ # Supabase Edge Functions (Deno/TypeScript)
├── micro-services/
│ └── emails-fetcher/ # Email fetching microservice (TypeScript/Bun)
├── docker-compose*.yml
└── generate_env.sh # Environment file generator
npm run install-deps # Install all dependencies
npm run dev:all # Start all services
npm run dev:generate-env # Generate .env files from .env.dev templates
npm run dev:supabase # Start local Supabase
npm run prettier:fix # Format all codecd backend
# Development
npm run dev:api # Start API server
npm run dev:worker # Start message worker
npm run dev:email-worker # Start email verification worker
npm run dev:email-signature-worker # Start signature extraction worker
# Testing
npm run test:unit # Run unit tests
npm run test:unit -- --testPathPattern=<file> # Run single test file
npm run test:unit -- --testNamePattern=<name> # Run tests matching name
npm run test:integration # Run integration tests
# Linting & Formatting
npm run lint # Lint code
npm run lint:fix # Fix lint issues
npm run prettier:fix # Format code
npm run build # Compile TypeScriptcd frontend
npm run dev # Start dev server
npm run test # Run Vitest tests
npm run test -- --run # Run tests once (not watch mode)
npm run test <file> # Run single test file
npm run lint # Lint code
npm run lint:fix # Fix lint issues
npm run prettier:fix # Format code
npm run build # Build for productioncd micro-services/emails-fetcher
bun run dev # Start dev server
npm run test:unit # Run unit tests
npm run lint # Lint code
npm run prettier:fix # Format code
npm run build # Compile TypeScriptnpm run dev:supabase-functions # Serve functions locally
npx supabase functions new <function-name> # Create new function (auto-generates structure)- Backend: Uses
tsconfig.jsonwithstrict: true,noImplicitAny: true - Frontend: Uses Nuxt's built-in TypeScript
- Supabase Functions: Deno with strict mode
- Files: kebab-case (e.g.,
mining-controller.ts,email-campaigns/) - Classes: PascalCase (e.g.,
TasksManager,RedisPublisher) - Functions: camelCase (e.g.,
getValidImapLogin,extractContacts) - Constants: SCREAMING_SNAKE_CASE (e.g.,
DEFAULT_SENDER_DAILY_LIMIT) - Interfaces/Types: PascalCase (e.g.,
MiningSource,ContactSnapshot)
Backend/Emails Fetcher (absolute imports via paths):
// Preferred - from package.json "paths" config
import Contacts from "../db/interfaces/Contacts";
import logger from "../utils/logger";
import ENV from "../config";
// External packages
import { NextFunction, Request, Response } from "express";
import { User } from "@supabase/supabase-js";Frontend:
// Vue/Nuxt patterns
import { useLeadminerStore } from "~/stores/leadminer";
import { useRuntimeConfig } from "#imports";Supabase Edge Functions:
import { Context, Hono } from "hono";
import { createSupabaseAdmin } from "../_shared/supabase.ts";Backend Express:
// Use custom error classes from utils/errors
import { ImapAuthError, ValidationError } from "../utils/errors";
// Controller pattern
async function controllerMethod(
req: Request,
res: Response,
next: NextFunction,
) {
try {
// ... logic
} catch (error) {
next(error); // Pass to error middleware
}
}Supabase Edge Functions:
// Return proper HTTP errors
return c.json({ error: "Failed to process campaign" }, 500);
// Use error boundaries in Edge Functions
try {
// ... logic
} catch (error) {
console.error("Error processing campaign:", error);
throw new Error(`Unable to fetch contacts for campaign: ${error.message}`);
}- Use Zod for runtime validation (backend, emails-fetcher, frontend config)
- Validate env vars at startup using schema.ts
- Use
validateTypehelper for runtime type checks
- Supabase: Use admin client (
createSupabaseAdmin) for privileged operations - PostgreSQL: Use parameterized queries, never string concatenation for SQL
- Redis: Use ioredis with connection pooling
- SECURITY DEFINER functions: Always set
SET search_path = ''to prevent search_path manipulation attacks (Supabase linter rulefunction_search_path_mutable) - SECURITY DEFINER functions: Always set
SET search_path = ''to prevent search_path manipulation attacks (Supabase linter rulefunction_search_path_mutable)
- Use winston logger in backend/microservices
- Include context (user ID, operation) in log messages
- Use appropriate levels:
error,warn,info,debug
Use the shared structured logger instead of raw console.log:
import { createLogger } from "../_shared/logger.ts";
const logger = createLogger("service-name");
// Structured JSON logging with context
logger.debug("Debug information", { userId: "123" });
logger.info("Operation completed", { userId: "123", duration: 150 });
logger.warn("Unexpected state", { details: context });
logger.error("Operation failed", { error: error.message });Best Practices:
- Output structured JSON for log aggregators (CloudWatch, Datadog)
- Control verbosity with
LOG_LEVELenvironment variable - Use English only for developer/operator logs (not user-facing)
- Always include relevant context (user ID, operation, IDs)
- Never log sensitive data (passwords, tokens, PII)
- Use appropriate levels:
debug: Development-only, detailed troubleshootinginfo: Operational events (token refresh, email sent)warn: Recoverable issues, unexpected stateserror: Failures requiring attention
Example Output:
{
"timestamp": "2026-03-02T14:30:00.000Z",
"service": "email-campaigns",
"level": "info",
"message": "OAuth token refreshed",
"email": "user@example.com"
}- Jest for backend and emails-fetcher
- Vitest for frontend
- Place tests in
test/unitortest/integrationdirectories - Match test file name:
controller.ts→controller.test.ts
- Branch naming:
feat/,fix/,docs/prefixes - Commit messages: imperative mood, lowercase first letter
- Run
npm run precommit:checkbefore committing (husky + lint-staged) - IMPORTANT - Ask User Before Git Operations:
- Ask before creating worktrees (using-git-worktrees skill)
- Ask before committing changes
- Ask before pushing to remote
- Ask before creating pull requests
- Never automatically commit or push without user approval
- Use Composition API with
<script setup> - Follow Nuxt 3 conventions (auto-imports in
composables/,utils/) - Use Pinia for state management
- Use PrimeVue components
- Use Hono framework
- Import shared code from
../_shared/directory - Always verify service role key for admin operations
- Handle CORS properly using shared cors headers
- All services read from
.envfiles - Use
.env.devas template (copied bynpm run dev:generate-env) - Important: Set
SUPABASE_PROJECT_URLto your public Supabase URL for self-hosted deployments
- Node.js >= 20.0.0 required
- Bun >= 1.1.0 required for emails-fetcher
- Docker and Docker Compose required for local dev
Leadminer uses a multi-repo deployment model with leadminer (source code) and leadminer.io (ops/infrastructure).
| Repo | Purpose | Deploy Target |
|---|---|---|
ankaboot-source/leadminer |
Source code (backend, frontend, micro-services) | QA & Prod servers |
ankaboot-source/leadminer.io |
Infrastructure (Docker Compose, Ansible, Terraform, CI workflows) | QA & Prod servers |
ankaboot-source/leadminer-commercial |
Commercial/billing features (merged via integration script) | QA & Prod servers |
Pushing to leadminer/main does NOT deploy directly. Instead, workflows in leadminer send repository_dispatch events to leadminer.io, which then runs the actual deployment workflows.
Why? The deployment workflows need access to secrets, server IPs, and infrastructure configs that live in leadminer.io.
Push to leadminer/main
│
├── backend/** or micro-services/** changed
│ └── triggers: qa-trigger-backend-build.yml
│ └── sends: repository_dispatch(type=trigger-qa-backend-build)
│ └── leadminer.io: qa-backend-build-deploy.yml
│ ├── builds Docker images (backend + workers)
│ └── SSH deploys to QA server
│
├── frontend/** changed
│ └── triggers: qa-trigger-frontend-build.yml
│ └── sends: repository_dispatch(type=trigger-qa-frontend-build)
│ └── leadminer.io: qa-frontend-build-deploy.yml
│ ├── builds frontend Docker image (pushed to Docker Hub)
│ └── SSH deploys to QA server
│
├── supabase/functions/** changed
│ └── triggers: push-supabase-functions.yml
│ └── deploys Edge Functions directly to Supabase project
│
└── other changes (docs, tests, etc.)
└── triggers: qa-trigger-no-build.yml
└── sends: repository_dispatch(type=trigger-qa-no-build)
└── leadminer.io: qa-no-build-deploy.yml
└── SSH deploys without building (pulls latest images)
Server: DigitalOcean droplet (LEADMINER_QA_IP)
Path: /home/leadminer-qa/
Docker Compose: Lives in leadminer.io/docker-compose.yml (templated via envsubst)
Deployment steps (both frontend and backend workflows):
- Clone/pull
leadminer.io(config repo) to~/leadminer.io - Clone/pull
leadminer(app repo) to~/leadminer_qa - Run
leadminer-integration.shto mergeleadminer-commercialbilling code - Template
.envanddocker-compose.ymlfromleadminer.ioconfigs - Backend deploy:
docker compose build --no-cachefor all services, thenup -d - Frontend deploy: Pull
ankabootorg/leadminer-frontend:latestfrom Docker Hub, thenup -d - Prune unused Docker images
Important: The backend builds from source (docker compose build) while the frontend pulls a pre-built image from Docker Hub. This means:
- Backend changes are built directly on the QA server
- Frontend changes must go through the build-and-push workflow first
Supabase (self-hosted) has its own deployment pipeline:
- QA:
supabase-deploy-qa.yml— deploys to QA Supabase server via SSH + Ansible - Prod:
supabase-deploy-prod.yml— deploys to Prod Supabase server - Migrations:
supabase-migrations-qa.yml/supabase-migrations-prod.yml
Supabase deployments are triggered by:
- Push to
mainwith changes insupabase/functions/**oransible/supabase-deployment/** - Manual
workflow_dispatch
Trigger: repository_dispatch(type=trigger-prod) or workflow_dispatch
Approval: Requires manual approval via GitHub issue (approvers: baderdean, zieddhf)
Server: DigitalOcean droplet (LEADMINER_PROD_IP)
Path: /home/leadminer/leadminer_prod
Prod workflow (deploy-prod.yml):
- Manual approval step
- Build frontend image (pushes to Docker Hub)
- SSH deploy to prod server (builds backend from source + pulls frontend image)
If you see a 404 for a newly added API endpoint (e.g., /api/smtp-senders/regenerate-from-sources):
- Check if the backend was rebuilt — The QA backend deploy (
qa-backend-build-deploy.yml) must have run AND succeeded after the merge. If only the frontend was deployed, the backend still runs the old code. - Check workflow status — Go to
leadminer.iorepo → Actions → check ifqa-backend-build-deployis green. - Check build output — The workflow does
docker compose build --no-cachewhich should pick up the latestmaincode. If the clone failed or cached an old commit, the new endpoints won't exist. - Force a re-deploy — If the workflow failed or was skipped, trigger it manually via
workflow_dispatchonqa-backend-build-deploy.yml.
- leadminer.io does NOT auto-deploy on leadminer/main push — It waits for the
repository_dispatchevent. - Frontend and backend deploys are separate workflows — A frontend-only change won't rebuild the backend, and vice versa.
- The frontend image is shared between QA and Prod — Both pull
ankabootorg/leadminer-frontend:latest. This means a prod deploy could accidentally get a QA-tested frontend if the image was overwritten. - leadminer-commercial is merged at deploy time — The integration script copies billing code into the repo before building. If commercial has breaking changes, the build will fail.
Local planning documents for development reference. Do NOT commit or push to remote.
When creating: docs/plans/YYYY-MM-DD-<feature-name>.md. Delete when feature is complete.
After committing and pushing changes, you may optionally run DeepSource to check for code quality issues.
Ask the user first: "Would you like me to run a DeepSource check on your changes?"
If the user agrees, use the deepsource-check skill to scan and fix issues.
# Check installation
which deepsource
# Check authentication
deepsource auth status
# Scan issues (after pushing)
deepsource issues --output json
# Scan specific PR
deepsource issues --pr <NUMBER> --output json
# Filter by severity
deepsource issues --severity critical --output json
deepsource issues --severity major --output jsonImportant: Prefer real code fixes over skipcq comments. Type assertions should use as unknown as Type instead of as any.
Before committing, run linting and formatting:
# Backend/Frontend
npm run lint:fix && npm run prettier:fix
# Or project-specific
cd backend && npm run lint:fix && npm run prettier:fix
cd frontend && npm run lint:fix && npm run prettier:fix
cd micro-services/emails-fetcher && npm run lint && npm run prettier:fixThis project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.
When the user types /graphify, invoke the skill tool with skill: "graphify" before doing anything else.
Rules:
- For codebase questions, first run
graphify query "<question>"when graphify-out/graph.json exists. Usegraphify path "<A>" "<B>"for relationships andgraphify explain "<concept>"for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output. - Dirty graphify-out/ files are expected after hooks or incremental updates; dirty graph files are not a reason to skip graphify. Only skip graphify if the task is about stale or incorrect graph output, or the user explicitly says not to use it.
- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
- After modifying code, run
graphify update .to keep the graph current (AST-only, no API cost).