Membership management system for Nobodies Collective (Spanish nonprofit).
Manage the full membership lifecycle for Nobodies Collective: volunteer applications are reviewed and approved by the Board, accepted members are provisioned into the appropriate teams and Google Workspace resources (Drive folders, Groups), and governance roles (Board, Coordinators, Admin) are tracked with temporal assignments. The system provides a way to organize teams logically and visually, gives Board and Admin visibility into what happens automatically on members' behalf through audit trails, and maintains GDPR compliance through consent tracking, data export, and right-to-deletion support.
See docs/architecture/design-rules.md for architectural rules:
- Services own their data — controllers cannot talk to DB, only services can
- Table ownership is strict — each service owns specific tables, no cross-service DB access
- Cache ownership follows data ownership — only the owning service manages its cache
- Cross-service calls via interfaces — need a profile? Call
IProfileService, don't query the table - Authorization via resource-based handlers — services are auth-free, controllers call
IAuthorizationService.AuthorizeAsync, noisPrivilegedbooleans
See docs/architecture/coding-rules.md for critical rules:
- Do not remove "unused" properties (reflection usage)
- Never rename fields in serialized objects (breaks JSON deserialization)
- JSON serialization requirements
- String comparison rules
- NodaTime for all dates/times (
Instant,LocalDate, etc.) - Every new page MUST have a nav link. If you add a controller action that returns a view, add a link to it from the nav menu or a contextual link from a related page. No orphan pages.
Clean Architecture with 4 layers:
- Domain: Entities, enums, value objects
- Application: Interfaces, DTOs, use cases
- Infrastructure: EF Core, external services, jobs
- Web: Controllers, views, API
See docs/architecture/data-model.md for full data model, relationships, and serialization notes. Key entities: User, Profile, ContactField, Application (Colaborador/Asociado tier applications), BoardVote (transient), RoleAssignment, LegalDocument/DocumentVersion, ConsentRecord (append-only), Team/TeamMember, GoogleResource, BudgetYear/BudgetGroup/BudgetCategory/BudgetLineItem, BudgetAuditLog (append-only), CityPlanningSettings, CampPolygon, CampPolygonHistory.
All Google Drive resources are on Shared Drives. This system does NOT use regular (My Drive) folders. All Drive API calls must use SupportsAllDrives = true, and permission listing must include permissionDetails to distinguish inherited from direct permissions. Only direct permissions are managed by the system — inherited Shared Drive permissions are excluded from drift detection and sync.
Google sync jobs (SystemTeamSyncJob hourly, GoogleResourceReconciliationJob daily at 03:00) are controlled by per-service mode at /Admin/SyncSettings (None/AddOnly/AddAndRemove). Set a service to "None" to disable without redeploying.
The consent_records table has database triggers that prevent UPDATE and DELETE operations. Only INSERT is allowed to maintain GDPR audit trail integrity.
Volunteer = the standard member. ~100% of users. Onboarding: sign up, complete profile, consent to legal docs, Consent Coordinator clears → auto-approved → added to Volunteers team. This is NOT done through the Application entity.
Colaborador = active contributor with project/event responsibilities. Requires application + Board vote. 2-year term.
Asociado = voting member with governance rights (assemblies, elections). Requires application + Board vote. 2-year term.
NEVER conflate Volunteer access with tier applications. The Application/Board Voting workflow is NOT part of volunteer onboarding. It is a separate, optional path for volunteers who want Colaborador or Asociado status. Volunteer access proceeds in parallel and is never blocked by tier applications.
The Application entity is for Colaborador and Asociado tier applications only, NOT for becoming a volunteer.
Submitted → Approved/Rejected
↘ Withdrawn ↙
Triggers: Approve, Reject, Withdraw
In all user-facing text (views, localization strings, emails), use "humans" — not "members", "volunteers", or "users". This is the org's branded terminology. It applies across all locales (the word "humans" is kept in English even in es/de/fr/it translations). Internal code (entity names, variable names) is unaffected.
Also: the system stores birthday (month + day only), not date of birth (which implies year). Use "birthday" in UI text.
Coolify strips .git from the Docker build context. Do NOT use COPY .git in the Dockerfile — it will fail on production deploys. Instead, Coolify passes SOURCE_COMMIT as a Docker build arg containing the full commit SHA. The Directory.Build.props MSBuild target for SourceRevisionId has a Condition to skip when the property is already set via -p:.
- Target scale: ~500 users total. This is a small nonprofit membership system, not a high-traffic service.
- Single server deployment — no distributed coordination, no multi-instance concerns. Database concurrency conflicts (e.g., DbContext thread safety) are irrelevant for parallelization decisions since there's only one process.
- Prefer in-memory caching over query optimization. At this scale, loading entire datasets into RAM (e.g., all teams, all members) is cheaper and simpler than optimizing individual DB queries. Use
IMemoryCachefreely. - Don't over-engineer for scale. Pagination, batching, and query optimization matter less when the total dataset fits comfortably in memory. Simple, correct code beats performant-but-complex code.
- No concurrency tokens. Do NOT add
IsConcurrencyToken(),[ConcurrencyCheck], or row versioning to any entity. At single-server scale with ~500 users, concurrency conflicts don't happen and optimistic concurrency only causes bugs. Never add them without explicit user permission.
Two-remote workflow:
origin=peterdrier/Humans(peter's fork — QA deploys frommain)upstream=nobodies-collective/Humans(production)
Critical: Always qualify issue and PR references with the repo. The two remotes have overlapping issue numbers, so bare #N is ambiguous and has caused real chaos (wrong issues closed, commits linked to the wrong tracker). Every reference — in commit messages, PR bodies, issue comments, release notes, todos, chat — must include the owner prefix:
- Fork:
peterdrier#292(orpeterdrier/Humans#292) - Upstream:
nobodies-collective#586(ornobodies-collective/Humans#586)
When invoking gh for issues/PRs, always pass --repo peterdrier/Humans or --repo nobodies-collective/Humans explicitly — never rely on the ambient default. If you don't know which repo a number belongs to, ask before writing it down.
Development flow:
- All changes go on a feature branch → PR to
mainon peter's fork (squash merge if multiple commits). Preview environments deploy per-PR at{pr_id}.n.burn.camp. Use a worktree under.worktrees/<name>. - Promote to production: batch changes on peter's
main, PR to nobodies'main(rebase merge, since individual efforts were already squashed going into peter'smain). - After production merge: reset peter's
mainto nobodies'main:git fetch upstream main git checkout main && git reset --hard upstream/main git push origin main --force-with-lease
QA deployment: Coolify auto-deploys on push to main on peter's fork. Coolify UI at https://coolify.n.burn.camp.
Preview environment details:
- URL:
https://{pr_id}.n.burn.camp - Database: cloned from QA via GitHub Action (
humans_pr_{N}), dropped on PR close - Auth: dev login enabled (
DevAuth__Enabled=true) since Google OAuth doesn't support wildcard redirect URIs - Connection string override:
docker-entrypoint.shextracts PR number fromCOOLIFY_CONTAINER_NAMEVersion endpoint:GET /api/version(unauthenticated) returns{ version, commit, informationalVersion }. Useful for checking which commit is deployed to a preview or QA environment.
dotnet build Humans.slnx
dotnet test Humans.slnx
dotnet run --project src/Humans.WebAfter running any recurring maintenance process (context cleanup, feature spec sync, NuGet check, code simplification, etc.), update docs/architecture/maintenance-log.md with the current date and next-due date.
| Topic | File |
|---|---|
| Design rules | docs/architecture/design-rules.md |
| Dependency graph | docs/architecture/dependency-graph.md |
| Coding rules | docs/architecture/coding-rules.md |
| Code review rules | docs/architecture/code-review-rules.md |
| Data model | docs/architecture/data-model.md |
| Analyzers/ReSharper | docs/architecture/code-analysis.md |
| Maintenance log | docs/architecture/maintenance-log.md |
| Feature specs | docs/features/ |
| Section invariants | docs/sections/ |
| Section template | docs/sections/SECTION-TEMPLATE.md |
| EF migration reviewer | .claude/agents/ef-migration-reviewer.md |
| Freshness catalog | docs/architecture/freshness-catalog.yml |
/freshness-sweep regenerates drift-prone docs against upstream/main diffs.
Catalog at docs/architecture/freshness-catalog.yml. Spec at
docs/superpowers/specs/2026-04-25-freshness-sweep-design.md.
Before committing any EF Core migration, run the EF migration reviewer agent (.claude/agents/ef-migration-reviewer.md). Mandatory for all database changes — do not commit or create PRs until it passes with no CRITICAL issues.
Important: When implementing new features, create or update the corresponding feature spec in docs/features/. Each feature doc should include:
- Business context
- User stories with acceptance criteria
- Data model
- Workflows/state machines (if applicable)
- Related features
The About page (Views/About/Index.cshtml) lists all production NuGet packages and frontend CDN dependencies with versions and licenses. After any NuGet package update, add the new package versions to the About page. This is tracked as a monthly maintenance task tied to the NuGet full update cycle.
The project is licensed under AGPL-3.0 (LICENSE at repo root).
After completing a fix or feature but before committing, check the relevant BRDs in docs/features/ and update them if the change affects documented behavior, authorization rules, workflows, data model, or routes. This reduces churn from separate doc-only commits.
docs/sections/ contains terse invariant documents for each major section of the app (Users, Profiles, Budget, Finance, Teams, Feedback, Camps, City Planning, Calendar, Governance, Legal & Consent, Onboarding, Google Integration, Shifts, Campaigns, Tickets, Auth, Email, Notifications, Audit Log). /Admin/* is a nav holder, not a section — its services belong to the sections they act on (Email, Profiles, Google Integration, Auth, Legal & Consent). Each doc defines:
- Concepts — the domain vocabulary the section owns
- Data Model — field-level detail for the entities this section owns (per-entity tables live here, not in
data-model.md) - Actors & Roles — who interacts and in what capacity
- Invariants — hard rules that must always be true (authorization, data integrity, workflow constraints)
- Negative Access Rules — the explicit "cannot" list
- Triggers — side effects and cascades ("when X happens, Y must happen")
- Cross-Section Dependencies — which other sections this section calls, by interface name
- Architecture — owning services, owned tables, migration status (A/B/C per
design-rules.md §15)
Every section follows the shape defined by docs/sections/SECTION-TEMPLATE.md. Copy it when creating a new section; keep existing sections aligned when editing.
When changing authorization attributes, role checks, or workflow logic in a section, verify the change is consistent with the section's invariant doc. Update the doc if the change intentionally alters an invariant.
Data-model ownership: each entity is owned by exactly one section. Per-entity field tables, indexes, constraints, and cross-domain FK strip status live in the owning section's ## Data Model block. docs/architecture/data-model.md is an index + cross-cutting rule sheet (FK graph between sections, append-only list, serialization rules) — do not duplicate per-entity content there.
After committing work that resolves or partially resolves items in todos.md, update the file: move completed items to the Completed section with a summary of what was done and the commit hash. This keeps the todo list accurate and avoids stale entries.
After committing work that resolves a GitHub issue, close the issue with gh issue close <number> -c "comment" including a brief summary and the commit hash.