- Notification entity with tenant isolation
- EF Core configuration with global query filter
- GET /api/notifications (list with pagination)
- GET /api/notifications/unread-count
- GET /api/notifications/{id}
- PUT /api/notifications/{id}/read (mark as read)
- PUT /api/notifications/read-all (mark all as read)
- DELETE /api/notifications/{id}
- Automatic notifications on connection request
- Automatic notifications on connection accepted
- Cross-tenant isolation verified
- All manual tests passing (see PHASE10_EXECUTION.md)
- Connection entity with tenant isolation
- EF Core configuration with global query filter
- GET /api/connections (list all connections)
- GET /api/connections/pending (incoming + outgoing)
- POST /api/connections (send request)
- PUT /api/connections/{id}/accept (accept request)
- PUT /api/connections/{id}/decline (decline request)
- DELETE /api/connections/{id} (remove/cancel)
- Mutual request auto-accept
- Cannot connect to yourself validation
- Cannot duplicate connection validation
- Cross-tenant isolation verified
- All manual tests passing (see PHASE9_EXECUTION.md)
- RefreshToken entity with tenant isolation
- PasswordResetToken entity with tenant isolation
- POST /api/auth/logout (revoke tokens)
- POST /api/auth/refresh (token rotation)
- POST /api/auth/register (new user registration)
- POST /api/auth/forgot-password (request reset)
- POST /api/auth/reset-password (confirm reset)
- Refresh token rotation for security
- Password reset invalidates all sessions
- Email sending (deferred - requires email service)
- All manual tests passing (see PHASE8_EXECUTION.md)
Started: 2026-02-02 Completed: 2026-02-02
Started: 2026-02-02 Completed: 2026-02-02
Started: 2026-02-02 Completed: 2026-02-02
Started: 2026-02-02 Completed: 2026-02-02
Started: 2026-02-02 Completed: 2026-02-02
Date: 2026-02-02 Status: Decided Context: The legacy PHP system uses MySQL/MariaDB. We need to choose a database for the new backend. Decision: Use PostgreSQL for the new backend. Rationale:
- Clean break from legacy - no temptation to share databases
- Better support for advanced features (JSONB, arrays, full-text search)
- Excellent EF Core support via Npgsql
- Industry standard for modern .NET applications
Consequences:
- Two separate databases during transition period
- Data synchronization will be needed later (out of scope for Phase 0)
- Cannot reuse PHP database schemas directly
Date: 2026-02-02 Status: Decided Context: The legacy migration documentation includes complex patterns (CQRS, MediatR, Clean Architecture). Decision: Start with the simplest possible architecture that meets Phase 0 objectives. Rationale:
- Prove core concepts before adding complexity
- Avoid premature abstraction
- Easier to debug and understand
- Can add patterns later when justified by actual needs
What we ARE using:
- Single Web API project
- EF Core directly in controllers (for now)
- Simple services where needed
- Global query filters for tenant isolation
What we are NOT using (yet):
- CQRS / MediatR
- Repository pattern
- Separate Application/Domain/Infrastructure projects
- AutoMapper
- FluentValidation pipeline behaviors
Date: 2026-02-02 Status: Decided Context: JWTs must be compatible with the legacy PHP system. Decision: Use the following claim structure:
{
"sub": "12345", // user_id as string
"tenant_id": 2, // integer
"role": "member", // string
"email": "user@example.com",
"iat": 1706889600, // issued at (unix timestamp)
"exp": 1738425600 // expires at (unix timestamp)
}Notes:
subclaim contains user ID (standard JWT practice)tenant_idis custom claim for multi-tenancy- Must use same signing key as PHP (HS256)
- Key stored in configuration, never in code
Date: 2026-02-02 Status: Decided Context: Need to determine tenant context for every request. Decision: Resolve tenant in the following order:
- JWT claim (
tenant_id) - for authenticated API requests - X-Tenant-ID header - for service-to-service calls
- Reject - if neither present, return 400
Rationale:
- JWT is the primary source of truth for authenticated users
- Header fallback allows testing and internal calls
- No domain-based routing in Phase 0 (complexity not justified yet)
- Explicit failure is safer than defaulting to a tenant
Date: 2026-02-02
Status: Decided
Context: User emails are unique per-tenant, not globally. A login with just email+password is ambiguous.
Decision: Login endpoint requires tenant_slug or tenant_id in addition to email and password.
Rationale:
- Emails may exist in multiple tenants (e.g., consultant working with multiple orgs)
- Tenant resolution must happen BEFORE user lookup
- Mirrors how PHP app handles login (tenant context from domain or explicit selection)
API Contract:
{
"email": "user@example.com",
"password": "secret",
"tenant_slug": "acme" // OR "tenant_id": 1
}Date: 2026-02-02 Status: Decided Context: Initial implementation allowed X-Tenant-ID header to set tenant context for any request. Decision: X-Tenant-ID header is ONLY allowed for unauthenticated requests in Development mode.
Rationale:
- Authenticated requests MUST use tenant_id from JWT - header cannot override
- Prevents privilege escalation where user could access another tenant's data
- Development-only header allows testing without valid JWT
- Production never trusts external headers for tenant context
Security Invariant: For authenticated requests, tenant context comes ONLY from JWT claims.
Assumption: The JWT signing secret from the PHP system will be provided via configuration. Risk if wrong: JWTs will not be interoperable. Mitigation: Verify with a real token from PHP before Phase 0 completion.
Assumption: Tenant IDs in the legacy system are integers (not GUIDs or strings). Risk if wrong: Schema mismatch, JWT claim parsing errors. Mitigation: Confirm with legacy database schema.
Assumption: A user belongs to exactly one tenant at a time. Risk if wrong: Authorization logic would need redesign. Mitigation: Verify with legacy data model.
Question: What are the exact token expiry durations used by PHP? Status: Open Impact: Must match for seamless interoperability. Notes: Documentation suggests: Web = 2 hours access, Mobile = 1 year access.
Question: How are refresh tokens stored and validated in PHP? Status: Open Impact: Affects whether we can validate PHP-issued refresh tokens. Notes: May need to defer refresh token support to later phase.
- CLAUDE.md created
- NOTES.md created
- Project structure created
- PostgreSQL connection working (Docker on port 5434)
- JWT generation working
- JWT validation working (for PHP-issued tokens)
- Tenant resolution middleware working
- Global query filter working
- One entity with tenant isolation (User)
- Health check endpoint
- Manual verification tests passed (see PHASE0_EXECUTION.md)
- Listing entity created with tenant isolation
- EF Core configuration with global query filter
- Listings migration generated
- ListingsController with GET endpoints
- Seed data for listings (5 listings across 2 tenants)
- All manual tests passing (see PHASE1_EXECUTION.md)
- Cross-tenant isolation verified
- PATCH /api/users/me endpoint added
- Validation for first_name and last_name (max 100 chars, no empty strings)
- Returns same shape as GET /api/users/me
- All manual tests passing (see PHASE2_EXECUTION.md)
- POST /api/listings endpoint added
- PUT /api/listings/{id} endpoint added
- DELETE /api/listings/{id} endpoint added (soft delete)
- Validation: title required, max 255 chars
- Validation: type must be "offer" or "request"
- Owner authorization (only owner can update/delete)
- Cross-tenant isolation (404 for other tenant's listings)
- All manual tests passing (see PHASE3_EXECUTION.md)
- Transaction entity created with tenant isolation
- EF Core configuration with global query filter
- Transactions migration generated
- WalletController with GET endpoints
- GET /api/wallet/balance (calculated from transactions)
- GET /api/wallet/transactions (with pagination and type filter)
- GET /api/wallet/transactions/{id} (participant check)
- Seed data for transactions (5 transactions across 2 tenants)
- Cross-tenant isolation verified
- All manual tests passing (see PHASE4_EXECUTION.md)
- POST /api/wallet/transfer endpoint added
- Validate: amount > 0
- Validate: sender != receiver
- Validate: receiver exists in same tenant
- Validate: sender has sufficient balance
- Returns new balance after transfer
- Transaction stored with status "completed"
- Cross-tenant isolation verified
- All manual tests passing (see PHASE5_EXECUTION.md)
- Conversation and Message entities created with tenant isolation
- EF Core configuration with global query filters
- GET /api/messages (list conversations with last message preview)
- GET /api/messages/{id} (conversation messages with pagination)
- GET /api/messages/unread-count (count of unread messages)
- Participant-only access (non-participants get 404)
- Seed data for 1 conversation with 5 messages
- Cross-tenant isolation verified
- All manual tests passing (see PHASE6_EXECUTION.md)
- POST /api/messages endpoint (send message, creates conversation if needed)
- PUT /api/messages/{id}/read endpoint (mark all messages in conversation as read)
- Validate: content required, max 5000 characters
- Validate: cannot message yourself
- Validate: recipient must exist in same tenant
- Conversation normalization (smaller participant ID first for uniqueness)
- Cross-tenant isolation verified
- All manual tests passing (see PHASE7_EXECUTION.md)
This section explicitly documents what is out of scope for Phase 0.
| Pattern | Reason for Exclusion |
|---|---|
| CQRS | Adds complexity without proven need; can add when read/write scaling requirements emerge |
| MediatR | Pipeline behaviors not needed yet; direct service calls are simpler to debug |
| Repository Pattern | EF Core DbContext already provides unit of work and abstraction |
| Clean Architecture layers | Single project is easier to reason about; split when boundaries become clear |
| AutoMapper | Manual mapping is explicit and avoids hidden bugs; can add when DTO count grows |
| FluentValidation | DataAnnotations sufficient for Phase 0; add pipeline behaviors when validation complexity grows |
| Feature | Reason for Exclusion |
|---|---|
| Refresh tokens | ✅ Implemented in Phase 8 |
| 2FA / MFA | Not required for trust establishment |
| Rate limiting | Add in Phase 1 when auth is stable |
| Audit logging | Add when compliance requirements are clear |
| Background jobs | No async processing needs in Phase 0 |
| Caching (Redis) | Optimize when needed, not preemptively |
| Real-time (SignalR/WebSockets) | Feature scope, not infrastructure scope |
| File uploads | Feature scope |
| Email/SMS notifications | Feature scope |
| Search (Elasticsearch) | Feature scope |
| API versioning | Single version for Phase 0 |
| Component | Reason for Exclusion |
|---|---|
| Docker for API | API runs on Kestrel locally; containerize when deployment pipeline is established (PostgreSQL uses Docker) |
| CI/CD pipelines | Set up with first production deployment |
| Kubernetes/orchestration | Premature; single instance is fine for Phase 0 |
| Monitoring (APM) | Built-in logging sufficient; add observability when in production |
| Database migrations in CI | Manual migrations acceptable for Phase 0 |
| Integration | Reason for Exclusion |
|---|---|
| PHP proxy (Strangler Fig) | This backend is independent; no routing from PHP needed yet |
| Database sharing with PHP | Separate databases by design (D001) |
| Data synchronization | Phase 0 is about proving isolation, not sync |
| Legacy code migration | This is a new codebase, not a migration |
| Test Type | Status |
|---|---|
| Unit tests | Add as code complexity warrants |
| Integration tests | One basic test to prove the stack works |
| E2E tests | Deferred to Phase 1 |
| Load tests | Deferred until performance requirements are defined |
- Legacy PHP documentation:
aspnet-migration/directory (read-only reference) - JWT spec: RFC 7519
- EF Core global filters: https://learn.microsoft.com/en-us/ef/core/querying/filters