Files
VolcanicCore/CLAUDE.md

14 KiB
Raw Blame History

VolcanicCore — project context for Claude sessions

VolcanicCore is a self-hosted N8N-style workflow engine (C# backend + React frontend). Operator draws a directed graph of nodes (triggers → actions → terminals) in a browser canvas; each workflow can be activated independently. When active, trigger nodes subscribe/listen and each event fires one execution through the graph.

Documents to read first

  • Design spec — docs/superpowers/specs/2026-08-28-volcaniccore-workflow-engine-design.md. The whole architecture (execution model, extension contracts, webhook mechanics, persistence, deploy shape). Read this before writing plans or code.
  • Plan 1 (backend foundation, executed) — docs/superpowers/plans/2026-08-28-plan-1-backend-foundation.md. Shows the working style and the file structure conventions.
  • Plan 2 (workflow engine, executed) — docs/superpowers/plans/2026-08-29-plan-2-workflow-engine.md. Executor, WebhookRouter, TriggerHost, five nodes, NATS provider, retention.
  • Plan 3 (React SPA, executed) — docs/superpowers/plans/2026-08-30-plan-3-react-spa.md. Vite/React/TS/Tailwind/React Flow SPA under frontend/, bundled into wwwroot.
  • Follow-ups — docs/superpowers/followups.md. Cross-plan items surfaced during review that later plans must handle. Read before starting a plan; extend as new items surface.

Current status

Plan 3 (React SPA) shipped. Head commit at plan close: 294bd57. dotnet build clean, dotnet test 74/74, pnpm --dir frontend test 20/20, pnpm --dir frontend test:e2e 1/1 (Playwright smoke).

What's done (in addition to Plans 1 + 2):

  • Vite + React 18 + TypeScript + Tailwind SPA under frontend/ (pnpm).
  • Same-origin cookie auth via apiFetch (credentials: "include", typed ApiError / UnauthorizedError).
  • TanStack Query for server state; useSession / useLogin / useLogout + query hooks for every endpoint.
  • Router w/ PrivateRoute gate. Pages: Login / Workflows / Editor / Connections / Executions.
  • React Flow (@xyflow/react) editor w/ draggable palette from /api/node-types, right-panel schema-driven config form (auto-generated from each ConfigSchema), connection picker, per-node position + connection_id persisted.
  • Editor toolbar: Save (PUT /api/workflows/{id}), Test run (POST /api/workflows/{id}/executions), Activate/Deactivate, Executions link.
  • Executions viewer: per-workflow run list, click → per-node status/timing/expandable IO JSON.
  • MSW-driven Vitest suite (20 tests across apiFetch, useSession, SchemaForm, LoginPage, WorkflowsPage, ConnectionsPage, graphAdapter).
  • pnpm build emits into src/VolcanicCore.Api/wwwroot/; .Api serves w/ UseStaticFiles + MapFallbackToFile("index.html") (API + /wh routes still win — 4 new xUnit tests cover the ordering).
  • Vite dev proxy /api + /wh → http://127.0.0.1:5099 so pnpm dev co-exists with dotnet run.
  • Playwright happy-path smoke in frontend/e2e/smoke.spec.ts — login → create workflow → drop node → save → reload.

Plan 4 (Container + Coolify deploy) shipped. Head commit at plan close: d237cc0. dotnet test 77/77 (3 new: 2× forwarded-headers + 1× admin-plaintext-reseed), Docker image builds and smoke-runs locally (~274 MB), live at https://core.tes.gd via Coolify.

What shipped in addition to Plans 1 + 2 + 3:

  • Multi-stage Dockerfile (web → api → runtime); runtime image mcr.microsoft.com/dotnet/aspnet:9.0 with curl for the compose healthcheck.
  • docker-compose.yml with expose: [8080], named volume vc-data on /data, SERVICE_FQDN_APP_8080 for Coolify/Traefik routing.
  • .dockerignore keeps bin/, obj/, frontend/node_modules/, frontend/dist/, src/VolcanicCore.Api/wwwroot/, and deploy.json out of the build context.
  • Program.cs wires UseForwardedHeaders (XForwardedProto + XForwardedFor, cleared KnownNetworks/KnownProxies) before UseAuthentication, so cookie SecurePolicy sees TLS behind Traefik.
  • AdminBootstrap.Seed catches both CryptographicException and FormatException on the legacy plaintext admin.password_hash_source row and re-seeds cleanly.
  • deploy.json (gitignored) holds non-secret Coolify config + secret env values; Coolify project uuid c2miztqvd95mnw5vcii2ezco, app uuid zp0h5ao841myjb8k8z5y5h6s, server uuid d0c4owww4kwo0ow08wws4ss8.

What's next: initial launch scope complete. Post-launch work goes to Plan 5 (CI + observability + DataProtection key persistence) — see docs/superpowers/followups.md.

Invariants (do NOT relearn from scratch)

  • Git commit identity. The repo has no configured identity. Every commit must pass -c user.email="claude@volcanic" -c user.name="Claude":
    git -c user.email="claude@volcanic" -c user.name="Claude" commit -m "..."
    
  • Working branch. main. The user opted in to this at project start. No feature branches unless requested.
  • Test parallelism is disabled at the assembly level. See the [assembly: CollectionBehavior(DisableTestParallelization = true)] attribute at the top of tests/VolcanicCore.Tests/WebFactory.cs. Multiple WebFactory instances share the process; pre-Configuration.Bind env-var reads made parallel classes flaky. Do not re-enable parallelism without a redesign.
  • Cookie authentication is in the shared framework. Do NOT add Microsoft.AspNetCore.Authentication.Cookies as a PackageReference. The last standalone version on NuGet is 2.3.0 (ASP.NET Core 2.x, 2019). .NET 9's Microsoft.AspNetCore.App shared framework provides the cookie-auth types with no PackageReference.
  • Program.cs env-var loop honors pre-set config. In tests, WebFactory.ConfigureWebHost uses builder.UseSetting(...) to inject DataDirectory and AdminPassword. The env-var loop in Program.cs skips keys already set by upstream sources (if (!string.IsNullOrEmpty(builder.Configuration[prop])) continue;). Do not remove this check.
  • Migration001Initial, 002Connections, 003Workflows, 004Executions, 005NodeExecutions are registered in the Migrator factory in Program.cs. Add new migrations to the same array in version order.
  • AdminBootstrap stores admin.password_hash_source in the admin_config table as the change-detector for the env-provided ADMIN_PASSWORD (see the comment in AdminBootstrap.cs). Do not "fix" this by hashing the source — it must be reversible so we can compare against the current env var on boot. The row is AES-GCM encrypted at rest as of Plan 2.
  • Fire-and-forget trigger sinks. TriggerActivationSink.EmitAsync returns Task.CompletedTask after spinning the execution into a background Task.Run. This prevents NATS pumps from blocking per-event on execution completion. WebhookRouter.Register (not the sink) is what the /wh endpoint awaits for the reply.
  • Cross-project cycle-avoidance for WebhookRegistry and WebhookRouter. Both types live in VolcanicCore.Core, NOT VolcanicCore.Api.Engine, because WebhookNode and WebResponseNode in .Nodes need them and .Api already references .Nodes (adding a back-reference would cycle). Do not move them back.
  • SPA bundles into src/VolcanicCore.Api/wwwroot/. Vite's build.outDir is repo-relative to frontend/ and points at the .Api project's wwwroot. The folder itself is .gitignore'd except for .gitkeep; the compiled JS/CSS is produced by pnpm build and consumed by dotnet publish (csproj has <Content Update="wwwroot\**\*" CopyToPublishDirectory="PreserveNewest" />). Do NOT commit wwwroot/*.js/*.css — CI produces them.
  • SPA fallback goes AFTER MapXxxEndpoints. MapFallbackToFile("index.html") in Program.cs must remain the last Map* call so /api/* and /wh/* still win. Static-file middleware (UseStaticFiles) goes right after UseAuthorization and is intentionally unauthenticated — the SPA itself sends users to /login when it sees a 401.
  • Frontend tsconfig has both references and paths. tsconfig.node.json is a composite project referenced from tsconfig.json, and needs noEmit: false + its own outDir (dist-tsconfig-node/, gitignored) to satisfy the composite constraint. tsconfig.json sets "paths": { "@/*": ["src/*"] } so tsc -b matches Vite's runtime @ alias — do NOT remove either.
  • vite.config.ts needs /// <reference types="vitest" /> on line 1. The test: block on defineConfig from "vite" won't typecheck without Vitest's config augmentation.
  • Docker runtime is aspnet:9.0, not distroless. Distroless has no curl (breaks the compose healthcheck) and hasn't been vetted against IronPython's P/Invoke surface. If you ever switch, install a healthcheck binary and test pnpm --dir frontend test:e2e against the container.
  • Vite outDir is ../src/VolcanicCore.Api/wwwroot (repo-relative to frontend/). The Dockerfile mirrors this layout — the web stage does mkdir -p src/VolcanicCore.Api/wwwroot under /build/ before running pnpm build, so Vite's .. path resolves cleanly. If you change vite.config.ts's build.outDir, update the Dockerfile in lockstep.
  • UseForwardedHeaders runs BEFORE UseAuthentication. Cookie SecurePolicy.SameAsRequest only flips the Secure flag when Request.IsHttps == true, which only happens once the forwarded-headers middleware has rewritten the scheme. Moving UseAuthentication above UseForwardedHeaders silently breaks HTTPS cookies in prod.
  • SERVICE_FQDN_APP_8080 (not SERVICE_FQDN_APP). Coolify v4 requires the port suffix in the env-var name AND in the docker_compose_domains[svc].domain string (https://core.tes.gd:8080). Without either, Traefik has no idea which container port to route to and returns HTTP 500.
  • deploy.json is gitignored — it carries secret app env values in .env. Never commit; if it ever gets committed, rotate every value in it.

Working style

  • Design with superpowers:brainstorming, plan with superpowers:writing-plans, execute with superpowers:subagent-driven-development. This is the tempo Plan 1 shipped on.
  • Split large scopes into multiple plans (Plan 1 was foundation, Plan 2 is engine, Plan 3 is frontend, Plan 4 is deploy). Each plan should end in a testable state.
  • TDD in each task: write the failing test, watch it fail, implement, watch it pass, commit. Migrator, connection CRUD, workflow CRUD all follow this pattern — see their commits for examples.
  • Two-stage review per task: spec-compliance reviewer (haiku), then code-quality reviewer (sonnet). For crypto or wire-format work, escalate quality-reviewer to opus. For pure interface/type tasks, one combined reviewer is fine.
  • Follow-ups. When a reviewer surfaces a real concern that isn't in scope for the current task, add it to docs/superpowers/followups.md with which plan should handle it. Do not silently drop review feedback.
  • Commit style. Conventional Commits (feat(scope):, fix(scope):, chore:, docs:). One meaningful change per commit; TDD tasks land one commit per task at the end (test + impl together).

How to run

# Build + test (backend)
dotnet build
dotnet test

# Live server (boot with a real SQLite DB)
DATA_DIRECTORY=$(pwd)/_smoke ADMIN_PASSWORD=hunter2 \
  dotnet run --project src/VolcanicCore.Api --urls http://127.0.0.1:5099
# Then: curl -c jar -X POST -H 'content-type: application/json' \
#         -d '{"password":"hunter2"}' http://127.0.0.1:5099/api/session
# And follow up with -b jar for authenticated calls.

dotnet run --project X sets the content root to X's directory, so DATA_DIRECTORY=./relative lands under src/VolcanicCore.Api/ — always pass an absolute path.

SPA dev loop

# Terminal A — backend on :5099
DATA_DIRECTORY=$(pwd)/_smoke ADMIN_PASSWORD=hunter2 \
  dotnet run --project src/VolcanicCore.Api --urls http://127.0.0.1:5099

# Terminal B — Vite dev server on :5173 (proxies /api and /wh to :5099)
pnpm --dir frontend dev

# Production-ish (SPA bundled + served by the API)
pnpm --dir frontend build
DATA_DIRECTORY=$(pwd)/_smoke ADMIN_PASSWORD=hunter2 \
  dotnet run --project src/VolcanicCore.Api --urls http://127.0.0.1:5099
# → open http://127.0.0.1:5099

Frontend tests:

pnpm --dir frontend test         # Vitest unit + integration (20 tests)
pnpm --dir frontend test:e2e     # Playwright smoke; needs `pnpm exec playwright install chromium` once

Deploy

Live: https://core.tes.gd

Trigger a redeploy (fire-and-forget per global CLAUDE.md rule):

# 1) MAKE SURE origin/main is at the ref you want deployed. Coolify's git
#    clone at build time reads from origin/main; local commits that haven't
#    been pushed don't ship. Push first, deploy second — always.
git push origin main
# 2) Trigger the deploy. Requires COOLIFY_URL, COOLIFY_KEY exported and
#    deploy.json populated.
APP=$(jq -r '.app.uuid' deploy.json)
curl -sS -X POST "$COOLIFY_URL/api/v1/deploy?uuid=$APP" \
  -H "Authorization: Bearer $COOLIFY_KEY" \
  | jq -r '.deployments[0].deployment_uuid // .deployment_uuid'
# → prints deployment_uuid; DO NOT poll. User verifies on their own time.

Env vars available in the container: COOLIFY_URL, COOLIFY_KEY, GIT_URL, GIT_USER, GIT_KEY. Container packaging + Coolify wiring shipped in Plan 4. Global CLAUDE.md rule applies: fire-and-forget deploys via the v4 API, never block on build.

Update env vars on the live app (e.g. rotate ADMIN_PASSWORD):

APP=$(jq -r '.app.uuid' deploy.json)
curl -sS -X PATCH "$COOLIFY_URL/api/v1/applications/$APP/envs" \
  -H "Authorization: Bearer $COOLIFY_KEY" -H 'Content-Type: application/json' \
  -d '{"key":"ADMIN_PASSWORD","value":"<new value>","is_preview":false}'