CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Repository layout#
Devlane is a monorepo with two deployable apps under apps/:
apps/api/— Go backend (Gin + GORM + PostgreSQL). Module pathgithub.com/Devlaner/devlane/api(kept stable; it is not tied to the folder location, so imports are unaffected by the directory name).apps/web/— React 19 + TypeScript + Vite SPA (Tailwind 4, React Router 7).planning/— Markdown design docs (architecture, phases, UI/UX plan). gitignored.docker-compose.yml— Local infra (Postgres, Redis, RabbitMQ, MinIO). The API connects to these via env vars.
Ignore any gitignored top-level reference directories you may see locally: treat their contents as unrelated third-party material that is not part of Devlane and is not built or run. Never read, import from, modify, or reference them (in code, commits, issues, or PRs). Implement everything natively in Go/React.
Commands#
Root-level (orchestrates both apps):
npm run validate # web typecheck + web lint + web prettier check + go vet + go testWeb (cd apps/web or npm --prefix apps/web run …):
npm run dev # vite dev server (default port 5173)
npm run build # tsc -b && vite build
npm run typecheck # tsc -b --noEmit
npm run lint # eslint .
npm run lint:fix
npm run format # prettier --write .
npm run format:check
npm run preview # serve production buildAPI (cd apps/api):
go run ./cmd/api # start API server (default :8080); auto-runs migrations on startup
go vet ./...
go test ./...
go test ./internal/auth -run TestMagicCode # single package / single testInfra:
docker compose up -d # postgres, redis, rabbitmq, minioPostgres is exposed on host port 15432 (not 5432). Set DB_PORT=15432 in apps/api/.env for local dev.
Commits & PRs#
- Conventional Commits are enforced by commitlint (
commit-msghook); header ≤ 100 chars. Use prefixes likefeat(scope):,fix(ui):,refactor(api):,chore:,docs:,test:,perf:,style:. - Don't commit to
main. Branch, open a PR, and let CI (api-ci/ui-ci) run. - AI-assisted contributions are welcome but MUST be disclosed. If an AI tool (Claude Code, Copilot, etc.) materially helped produce a change:
- Add a trailer to each AI-assisted commit, e.g.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>. - State it in the PR description — the PR templates have an AI assistance section; tick it and name the tool(s).
- This is a transparency requirement, not a barrier. See CONTRIBUTING.md.
- Add a trailer to each AI-assisted commit, e.g.
Git hooks (Husky)#
pre-commit:lint-staged(ESLint+Prettier on staged web files,gofmton staged Go files) → if any web changes also runsweb typecheck + lint→ if any Go changes also runsgo vet + go test.pre-push: web typecheck +go test ./....commit-msg: enforces Conventional Commits (@commitlint/config-conventional, header ≤100 chars).
Don't bypass with --no-verify — fix the underlying lint/type/test failure instead.
Backend architecture (apps/api/)#
Layered, dependency-injected from cmd/api/main.go → internal/router/router.go. The router is the single composition root; reading it gives you the full surface area.
cmd/api/main.go # wires config, db, redis, rabbitmq, minio, then router
internal/
├── config/ # env loading (godotenv)
├── database/ # GORM connection + golang-migrate runner
├── middleware/ # Recovery, Logger, CORS, RequireAuth
├── auth/ # session service, magic-code (HMAC) login
├── oauth/ # google, github, gitlab providers (resolved per-request from instance settings)
├── github/ # GitHub App integration: client, installations, webhooks, ref parsing
├── crypto/ # password hashing, token generation
├── model/ # GORM models (one file per entity)
├── store/ # Data-access layer (one store per model). Pure DB.
├── service/ # Business logic. Composes stores; enforces workspace/project membership.
├── handler/ # Gin handlers. HTTP shape only — bind, call service, return JSON.
├── router/router.go # Builds *gin.Engine; declares ALL routes
├── redis/ rabbitmq/ queue/ # Optional integrations — services degrade gracefully if absent
├── mail/ # SMTP sender; reads instance_settings for credentials
└── minio/ # File upload/serve (covers, avatars, logos)
Key conventions:
- Layering rule: handler → service → store. Handlers never touch GORM directly; stores never call services.
- URL nesting mirrors the data model:
/api/workspaces/:slug/projects/:projectId/issues/:pk/.... Trailing slashes are intentional and match the web app's expectations — keep them. - Auth model: cookie sessions for browsers; bearer tokens accepted by the same
RequireAuthmiddleware. OAuth provider config is stored ininstance_settingsand resolved at request time, not from env. - Optional infra is optional: Redis, RabbitMQ, and MinIO failures at startup are logged as warnings, not fatal. Code paths that require them must check for
nil. Don't introduce hard dependencies on these. - Migrations:
apps/api/migrations/NNNNNN_<name>.{up,down}.sql, loaded bydatabase.RunMigrationsvia the golang-migrate file source. Auto-applied at startup. Add both up and down files; never edit a migration after it's been merged. - Instance setup is first-run:
/api/instance/setup-status/and/api/instance/setup/(no auth) seed the singleton instance. Feature flags, SMTP, and OAuth creds live in theinstance_settingstable — admin UI inapps/web/src/pages/instance-admin/writes them. - Background work uses RabbitMQ via
queue.Publisher(publish) andqueue.Consumer(subscribe in the API process itself — there is no separate worker binary). Currently handlesQueueEmailsandQueueWebhooks.
Frontend architecture (apps/web/)#
src/
├── App.tsx # ThemeProvider → AuthProvider → FavoritesProvider → RouterProvider
├── routes/index.tsx # createBrowserRouter — all routes, lazy-loaded pages
├── api/client.ts # Axios instance; baseURL from VITE_API_BASE_URL (defaults to http://localhost:8080 in dev)
├── services/ # One file per resource (issueService, workspaceService, …) — thin wrappers over apiClient
├── pages/ # Route components (lazy)
├── components/ # Domain components (work-item/, project-issues/, stickies/, …) and primitives (ui/)
├── contexts/ # AuthContext, ThemeContext, FavoritesContext, WorkspaceViewsState, ProjectSavedViewDisplay, ModulesFilter
├── hooks/ lib/ utils/ # Pure helpers, event buses, slug/date utils
└── types/ # Cross-cutting TS types
Key conventions:
- Routing is workspace-scoped:
/:workspaceSlug/.... Most pages readuseParams<{ workspaceSlug; projectId }>()and call services with those keys.RootRedirecthandles the bare/after login by redirecting to the user's last/first workspace;SetupGateguards everything until instance setup is complete. - Auth gating:
<ProtectedRoute>wraps theAppShell-based layout;<InstanceAdminProtectedRoute>wraps the instance-admin tree.<AuthProvider>calls/api/users/me/on mount. - API calls: always go through a service in
src/services/, which usesapiClientfromsrc/api/client.ts.withCredentials: trueis required for cookie sessions. Don't create new axios instances. - Cross-component coordination: small DOM-event buses in
src/lib/*Events.ts(e.g.homeWidgetsEvents,projectIssuesEvents) for state that doesn't justify a full context. Prefer extending an existing event bus to inventing a new context. - Styling: Tailwind v4 (CSS-first config in
index.css/styles/). CSS variables drive theming — patterns liketext-(--txt-tertiary)reference custom properties set byThemeContext. Useclsx+tailwind-merge(cnhelper inlib/utils.ts). - Editor: TipTap 3.22.3 — keep all
@tiptap/*packages on the same exact version (they share peer deps; mismatches break silently). - Lazy boundaries: every page is
lazy()with a<Suspense fallback={<PageFallback />}>. New pages should follow the same pattern inroutes/index.tsx.
Working with this repo#
- When adding an API endpoint, the change usually touches:
model/(if new entity) →store/→service/→handler/→ register inrouter/router.go→ corresponding service inapps/web/src/services/→ web consumer. - Trailing slashes in routes matter — the web app's services expect them. Add both
/pathand/path/only when the existing handler does (seeissueViewHandler.AddFavoritefor the pattern). - New env vars: add to
internal/config/config.go(Configstruct +Load), then thread throughrouter.Config. Document the default and whether it's optional.