PHASE 1: Core Backend v1.0
Trigger: "Execute Phase 1 as per masterplan."
Goal: The core backend is functional: entities, authentication (with ~16 roles and permission table), members, contracts, and products can be managed via REST API. Database schema is created via Flyway. OpenAPI documentation is generated.
Prerequisite: Phase 0 is completed.
Step 1.0 — Technical Conception
Create doc/developer/conception/phase-1-conception.md.
(reference: Chapter 06 — Module Architecture, Chapter 11 — Data Model, Chapter 13 — Security)
Content:
- Scope: 5 modules (entity, auth, member, contract, product) + OpenAPI setup
- Component Design: Mermaid diagram showing module dependencies and data flow
- Interface Contracts: REST endpoint signatures for all 5 modules (from Chapter 10)
- Data Flow: Request -> Controller -> Service -> Repository -> DB; JWT filter chain
- Entity Design: All fields from Chapter 11 for Organization, User, Member, MembershipTemplate, Contract, Product, AuditLog
- Error Handling: GlobalExceptionHandler patterns, error codes, i18n error messages
- Edge Cases: Concurrent contract purchase, member number generation race condition, bcrypt timing attacks
- Security: JWT RS256 flow, refresh token rotation, brute-force protection (Redis counter)
- Roles & Permissions: Permission evaluation flow (JWT claims -> Spring Security -> @PreAuthorize)
- Acceptance Criteria: Checklist matching Phase 1 Quality Gate
- Design Chapter References: Ch06, Ch07, Ch10, Ch11, Ch13
Result: Technical conception document for Phase 1.
Step 1.1 — Entity/Organization Module
Create the membership-entity module.
(reference: Chapter 11 — Data Model, Chapter 10 — API Design)
Approach:
- Create JPA entity:
Organization-@Versionfor optimistic locking -@EntityListeners(AuditingEntityListener.class)for createdAt/updatedAt - JSONB columns with@JdbcTypeCode(SqlTypes.JSON)for settings and custom attributes - Fields (from Chapter 11):
| Field | Type | Constraints | Description |
|---|---|---|---|
| id | Long | PK, generated | Primary key |
| name | String(200) | NOT NULL | Organization display name |
| slug | String(100) | UNIQUE, NOT NULL | URL-safe identifier (auto-generated from name) |
| parentId | Long | FK nullable | Parent organization (franchise hierarchy) |
| type | String(30) | NOT NULL | CLUB / FRANCHISE / MANUFACTURER |
| street | String(200) | Address line | |
| city | String(100) | City | |
| postalCode | String(20) | Postal/ZIP code | |
| country | String(2) | NOT NULL, default 'DE' | ISO 3166-1 alpha-2 |
| state | String(100) | State/province | |
| contactEmail | String(200) | NOT NULL | Primary contact email |
| contactPhone | String(50) | Phone (E.164 format) | |
| website | String(500) | Website URL | |
| logoUrl | String(500) | Logo image URL (stored in S3) | |
| timezone | String(50) | default 'Europe/Berlin' | IANA timezone |
| locale | String(10) | default 'de_DE' | Default locale |
| currency | String(3) | default 'EUR' | ISO 4217 currency code |
| tier | String(20) | default 'STARTER' | STARTER / TEAM / PROFESSIONAL / ENTERPRISE |
| status | String(20) | default 'TRIAL' | TRIAL / ACTIVE / SUSPENDED / DEACTIVATED |
| trialExpiresAt | Instant | nullable | Trial expiration date |
| billingDay | Integer | default 1 | Day of month for billing cycle (1-28) |
| settings | JSONB | default '{}' | Extensible settings (password policy, feature toggles, branding) |
| customAttributeDefinitions | JSONB | default '[]' | Custom member attribute schema |
| createdAt | Instant | auto | Creation timestamp |
| updatedAt | Instant | auto | Last modification |
| version | Long | @Version | Optimistic lock |
- Settings JSONB schema:
json { "passwordPolicy": { "minLength": 8, "requireUppercase": true, "requireNumber": true }, "featureToggles": { "shopEnabled": false, "crmEnabled": false }, "branding": { "primaryColor": "#2CC5CE", "accentColor": "#FF5722" }, "homepageConfig": { "heroTitle": "", "showPricing": true, "showSchedule": true } }
- Create DTOs:
OrganizationDto,CreateOrganizationRequest,UpdateOrganizationRequest,OrganizationSettingsDto - Create repository:
OrganizationRepository extends JpaRepository- Custom queries:findBySlug,findByParentId,searchByNameContaining - Create service:
OrganizationService- CRUD operations - Parent-child hierarchy traversal (getChildren,getAncestors) - Settings management (JSONB merge, not overwrite) - Slug generation (transliterate + lowercase + deduplicate) - Tier enforcement (feature access based on tier) - Create controller:
OrganizationController-POST /api/entity— Create organization (SUPER_ADMIN only) -GET /api/entity/{id}— Get entity details -GET /api/entity/search?name={query}&type={type}&page={n}&size={n}— Search entities (paginated) -PUT /api/entity/{id}— Update entity -GET /api/entity/{id}/children— List child entities (franchise) -GET /api/entity/{id}/settings— Get entity settings -PUT /api/entity/{id}/settings— Update entity settings (JSONB merge) - Create Flyway migration:
V100__create_organization.sql- Table, sequence, indexes on slug (unique), parentId, status, type - Create test:
OrganizationServiceTest(JUnit 5, Mockito) - Tests: CRUD, slug generation, hierarchy traversal, settings merge, tier enforcement
Validation:
cd backend && ./mvnw test -pl membership-entity
Result: Organization entity with CRUD API, Flyway migration, and tests.
Step 1.2 — Authentication Module
Create the membership-auth module.
(reference: Chapter 13 — Security and Compliance, Chapter 07 — User Roles, ADR-AUTH-001)
Approach:
- Create JPA entities:
User,UserRole- User fields (from Chapter 11):
| Field | Type | Constraints | Description |
|---|---|---|---|
| id | Long | PK, generated | Primary key |
| entityId | Long | FK NOT NULL | Organization reference |
| String(200) | NOT NULL | Login email (unique per entity) | |
| passwordHash | String(200) | NOT NULL | bcrypt hash (cost factor 12) |
| firstName | String(100) | NOT NULL | First name |
| lastName | String(100) | NOT NULL | Last name |
| phone | String(50) | Phone (E.164) | |
| locale | String(10) | default 'en' | Preferred language for UI and emails |
| status | String(30) | default 'PENDING_VERIFICATION' | PENDING_VERIFICATION / ACTIVE / LOCKED / DEACTIVATED |
| emailVerifiedAt | Instant | nullable | When email was verified |
| lastLoginAt | Instant | nullable | Last successful login |
| failedLoginCount | Integer | default 0 | Failed login attempts (reset on success) |
| lockedUntil | Instant | nullable | Lock expiration (brute-force protection) |
| refreshTokenHash | String(200) | nullable | SHA-256 hash of current refresh token |
| refreshTokenExpiresAt | Instant | nullable | Refresh token expiration |
| totpSecret | String(200) | nullable | TOTP secret for 2FA (encrypted at rest) |
| totpEnabled | boolean | default false | Whether 2FA is active |
| createdAt | Instant | auto | |
| updatedAt | Instant | auto | |
| version | Long | @Version | Optimistic lock |
- UserRole: join table (userId, roleCode) with composite PK
- Unique constraint: (entityId, email) — same email allowed in different organizations
-
Create DTOs: -
LoginRequest(email, password, totpCode?) -LoginResponse(accessToken, refreshToken, expiresIn, user: {id, email, firstName, lastName, roles[], permissions[], locale, entityId}) -RegisterRequest(email, password, firstName, lastName, entityId, locale?) -ChangePasswordRequest(currentPassword, newPassword) -ForgotPasswordRequest(email) -ResetPasswordRequest(token, newPassword) -UserDto,UpdateUserRequest -
Create service:
AuthService- Register: validate email uniqueness, hash password, create user with PENDING_VERIFICATION, send verification email (via Communication queue) - Login: validate credentials, check status/lock, increment failedLoginCount on failure, generate tokens - Verify email: validate token (UUID, 24h expiry), set emailVerifiedAt, activate user - Resend verification: rate limited (1 per minute) - Forgot password: generate reset token (UUID, 1h expiry), send via email - Reset password: validate token, hash new password, invalidate all sessions - Refresh token: validate refresh token hash, rotate (issue new pair, invalidate old) - Logout: clear refresh token hash from DB - Brute-force protection (Redis):- Key:
auth:failed:{entityId}:{email}, TTL: 15 min - Lock after 5 failures, unlock after 15 min
- Return same error for wrong email and wrong password (prevent enumeration)
- Key:
-
Create
JwtProvider(RS256 asymmetric signing): - Key pair from configuration files:JWT_PRIVATE_KEY_PATH,JWT_PUBLIC_KEY_PATH(NEVER hardcoded) - Access token: 15 min TTL - Refresh token: 30 days TTL, stored as SHA-256 hash in DB - JWT claims structure:json { "sub": "user-123", "iss": "membership-one", "iat": 1708000000, "exp": 1708000900, "entityId": 456, "roles": ["CLUB_ADMIN", "ACCOUNTANT"], "permissions": ["MEMBER_READ", "MEMBER_WRITE", "BILLING_READ", "BILLING_EXECUTE", "ACCOUNTING_READ"], "locale": "de", "tier": "PROFESSIONAL" }- Refresh token: opaque UUID, only hash stored server-side -
Create
JwtAuthenticationFilterandPermissionEvaluator: - Filter: extract Bearer token, validate signature + expiration, set SecurityContext -@PreAuthorize("hasPermission('MEMBER_WRITE')")on controller methods - Permissions loaded from JWT claims (no DB lookup per request) - Tenant isolation: all queries filtered byentityIdfrom JWT claims -
Create controller:
AuthController(11 endpoints) -POST /api/auth/register— Register new user -POST /api/auth/login— Login (returns tokens) -POST /api/auth/verify-email?token={token}— Verify email address -POST /api/auth/resend-verification— Resend verification email -POST /api/auth/forgot-password— Request password reset -POST /api/auth/reset-password— Reset password with token -POST /api/auth/refresh— Refresh access token -POST /api/auth/logout— Invalidate refresh token -POST /api/auth/change-password— Change password (authenticated) -GET /api/auth/me— Get current user profile -PUT /api/auth/me— Update current user profile (name, locale, phone) -
Create Flyway migrations:
V101__create_user.sql,V102__create_user_role.sql- Indexes: (entityId, email) unique, status, lastLoginAt - user_role: composite PK (user_id, role_code), FK to role seed table -
Create tests:
AuthServiceTest,JwtProviderTest,PermissionEvaluatorTest- AuthServiceTest: register, login success/failure, brute-force lock, verify email, refresh rotation - JwtProviderTest: generate, validate, expired, tampered, claims extraction - PermissionEvaluatorTest: has permission, missing permission, role hierarchy
Result: Complete auth system with JWT RS256, ~16 roles, permission matrix, brute-force protection.
Step 1.3 — Member Management Module
Create the membership-member module.
(reference: Chapter 11 — Data Model, Chapter 10 — API Design)
Approach:
- Create JPA entities:
Member,AuditLog- Member fields (from Chapter 11):
| Field | Type | Constraints | Description |
|---|---|---|---|
| id | Long | PK, generated | |
| entityId | Long | FK NOT NULL | Organization |
| memberNumber | String(30) | UNIQUE per entity | Auto-generated (entity prefix + sequence) |
| firstName | String(100) | NOT NULL | |
| lastName | String(100) | NOT NULL | |
| String(200) | Contact email (not login email) | ||
| phone | String(50) | E.164 format | |
| dateOfBirth | LocalDate | Required for age-restricted memberships | |
| gender | String(20) | MALE / FEMALE / DIVERSE / UNSPECIFIED | |
| street | String(200) | ||
| city | String(100) | ||
| postalCode | String(20) | ||
| country | String(2) | default 'DE' | ISO 3166-1 |
| photoUrl | String(500) | Profile photo (S3 URL) | |
| emergencyContactName | String(200) | Required for minors | |
| emergencyContactPhone | String(50) | ||
| responsiblePersonId | Long | FK nullable | Legal guardian for minors |
| customAttributes | JSONB | default '{}' | Entity-defined custom fields |
| notes | String(2000) | Internal admin notes | |
| status | String(20) | default 'ACTIVE' | ACTIVE / INACTIVE / SUSPENDED |
| joinDate | LocalDate | NOT NULL | Date member joined |
| exitDate | LocalDate | nullable | Date member left |
| userId | Long | FK nullable | Linked user account (for self-service) |
| createdAt | Instant | auto | |
| updatedAt | Instant | auto | |
| version | Long | @Version |
- AuditLog: id, entityId, entityType (MEMBER/CONTRACT/ORGANIZATION), entityIdRef, action (CREATE/UPDATE/DELETE), changedFields (JSONB: {field: {old, new}}), performedBy, performedAt
- Create DTOs:
MemberDto,CreateMemberRequest,UpdateMemberRequest,MemberSearchCriteria,MemberListItemDto(lightweight for table views) - Create repository:
MemberRepositorywith custom queries -searchByEntityIdAndCriteria(Long entityId, MemberSearchCriteria criteria, Pageable pageable)- Criteria: name (full-text), email, status, memberNumber, joinDate range, membership template - Create service:
MemberService- CRUD (create, read, update, soft-delete), search with pagination - Emergency contacts, responsible person (minors), custom attributes (JSONB) - Member number generation:SELECT nextval('member_number_seq_{entityId}') FOR UPDATE- Format: configurable per entity (default:
M-{5-digit-sequence}, e.g.,M-00001) - Audit logging on all mutations (AuditLog entry with JSON diff)
- Custom attribute validation against entity's
customAttributeDefinitions - Age calculation for age-restricted plans
- Link member to user account (for self-service portal)
- Format: configurable per entity (default:
- Create controller:
MemberController-POST /api/members— Create member -GET /api/members/{id}— Get member details -PUT /api/members/{id}— Update member -DELETE /api/members/{id}— Soft-delete (set status INACTIVE, set exitDate) -GET /api/members/search?name={q}&status={s}&page={n}&size={n}&sort={field,dir}— Search (paginated) -GET /api/members/{id}/audit— Get audit history -POST /api/members/{id}/link-user— Link member to user account - Create Flyway migrations:
V103__create_member.sql,V104__create_audit_log.sql- Indexes: (entityId, memberNumber) unique, (entityId, lastName, firstName), (entityId, email), (entityId, status) - Sequence per entity for member numbers (created dynamically by service) - Create tests:
MemberServiceTest- Tests: CRUD, search with pagination, member number generation (concurrent), custom attributes, audit log, age validation
Result: Member management with search, pagination, custom attributes, and audit trail.
Step 1.4 — Contract/Membership Module
Create the membership-contract module.
(reference: Chapter 11 — Data Model, Chapter 10 — API Design)
Approach:
- Create JPA entities:
MembershipTemplate,Contract- MembershipTemplate fields:
| Field | Type | Constraints | Description |
|---|---|---|---|
| id | Long | PK | |
| entityId | Long | FK NOT NULL | Organization |
| name | String(200) | NOT NULL | e.g., "Premium Membership" |
| description | String(2000) | Public description | |
| price | BigDecimal(19,4) | NOT NULL | Monthly price |
| currency | String(3) | default 'EUR' | ISO 4217 |
| billingCycle | String(20) | NOT NULL | MONTHLY / QUARTERLY / SEMI_ANNUAL / ANNUAL |
| minimumTermMonths | Integer | default 1 | Minimum contract duration |
| cancellationNoticeDays | Integer | default 30 | Days notice before term end |
| autoRenew | boolean | default true | Auto-renew at term end |
| ageMin | Integer | nullable | Minimum age (null = no restriction) |
| ageMax | Integer | nullable | Maximum age |
| maxMembers | Integer | nullable | Capacity limit (null = unlimited) |
| visible | boolean | default true | Show in public catalog |
| sortOrder | Integer | default 0 | Display order |
| features | JSONB | default '[]' | Included features list |
| status | String(20) | default 'ACTIVE' | ACTIVE / ARCHIVED |
| createdAt | Instant | auto | |
| updatedAt | Instant | auto | |
| version | Long | @Version |
- Contract fields:
| Field | Type | Constraints | Description |
|---|---|---|---|
| id | Long | PK | |
| entityId | Long | FK NOT NULL | |
| memberId | Long | FK NOT NULL | |
| templateId | Long | FK NOT NULL | MembershipTemplate |
| status | String(20) | NOT NULL | See state machine below |
| startDate | LocalDate | NOT NULL | Contract start |
| endDate | LocalDate | nullable | Term end (null = open-ended after minimum) |
| cancelledAt | Instant | nullable | When cancellation was requested |
| cancelledBy | Long | nullable | User who cancelled |
| cancellationReason | String(500) | nullable | |
| monthlyPrice | BigDecimal(19,4) | NOT NULL | Locked-in price at purchase time |
| currency | String(3) | default 'EUR' | |
| signedAt | Instant | nullable | Digital signature timestamp |
| signatureIp | String(50) | nullable | IP at signing (audit) |
| signatureUserAgent | String(500) | nullable | User agent at signing (audit) |
| createdAt | Instant | auto | |
| updatedAt | Instant | auto | |
| version | Long | @Version |
- Contract status state machine:
PENDING_SIGNATURE → ACTIVE → CANCELLED → EXPIRED (auto, at endDate) → SUSPENDED (admin action, e.g., payment failure) SUSPENDED → ACTIVE (resumed) → CANCELLED (if not resolved)
- Create DTOs, repositories, services:
-
MembershipTemplateService— CRUD, visibility management, capacity check, pricing history -ContractService:purchase(memberId, templateId)— Validate age, capacity, create PENDING_SIGNATURE contractsign(contractId, signatureData)— Record signature, activate contract, schedule first billingcancel(contractId, reason)— Validate cancellation notice period, set cancelledAtsuspend(contractId)— Admin suspend (e.g., payment failure)resume(contractId)— Resume from SUSPENDEDexpire()— Scheduled: find contracts past endDate, set EXPIREDrenew()— Scheduled: auto-renew contracts approaching endDate (if autoRenew = true)
- Create controllers:
- MembershipTemplateController:
POST /api/membership-templates— Create template (CLUB_ADMIN)GET /api/membership-templates— List templates for entity (admin)GET /api/membership-templates/catalog/{entityId}— Public catalog (visible only)PUT /api/membership-templates/{id}— Update templateDELETE /api/membership-templates/{id}— Archive template- ContractController:
POST /api/contracts— Purchase membership (creates contract)POST /api/contracts/{id}/sign— Sign contractGET /api/contracts/{id}— Get contract detailsPOST /api/contracts/{id}/cancel— Cancel contractGET /api/contracts/member/{memberId}— List contracts for member
- Business rules: - Cancellation notice: contract cannot be cancelled if remaining days < cancellationNoticeDays - Auto-renewal: create new contract with same template at term end - Age restrictions: member's age must be between template.ageMin and template.ageMax - Capacity: count active contracts per template, reject if >= maxMembers - Price lock: contract stores price at purchase time (template price changes don't affect existing)
- Create Flyway migrations:
V105__create_membership_template.sql,V106__create_contract.sql- Indexes: (entityId, status) on both, (memberId) on contract, (templateId) on contract - Create tests:
MembershipTemplateServiceTest,ContractServiceTest- ContractServiceTest: purchase, sign, cancel within notice, cancel past notice (rejected), expire, renew, suspend/resume, age restriction, capacity limit
Result: Full membership lifecycle with state machine, cancellation rules, auto-renewal, and price locking.
Step 1.5 — Product/Service Module
Create the membership-product module.
(reference: Chapter 11 — Data Model, Chapter 10 — API Design)
Approach:
- Create JPA entity:
Product- Product fields:
| Field | Type | Constraints | Description |
|---|---|---|---|
| id | Long | PK | |
| entityId | Long | FK NOT NULL | Organization |
| name | String(255) | NOT NULL | e.g., "Personal Training Session" |
| description | String(2000) | Public description | |
| categoryCd | String(50) | NOT NULL | COURSE, PERSONAL_TRAINING, MERCHANDISE, RENTAL, ADD_ON, DAY_PASS |
| typeCd | String(50) | NOT NULL | ONE_TIME, RECURRING, PER_SESSION |
| statusCd | String(50) | default 'ACTIVE' | ACTIVE / ARCHIVED / DRAFT |
| price | BigDecimal(19,4) | NOT NULL | Base price |
| currency | String(3) | default 'EUR' | ISO 4217 |
| vatRate | BigDecimal(5,2) | NOT NULL | e.g., 19.00 or 7.00 |
| vatIncluded | boolean | default true | Price includes VAT |
| maxCapacity | Integer | nullable | Capacity limit (courses, events) |
| durationMinutes | Integer | nullable | Session/class duration |
| isShopItem | boolean | default false | Available in member shop |
| stockQuantity | Integer | nullable | Inventory count (null = unlimited) |
| sortOrder | Integer | default 0 | Display order in catalog |
| imageUrl | String(500) | nullable | Product/course image |
| customAttributes | JSONB | default '{}' | Extensible fields (SKU, difficulty, etc.) |
| visible | boolean | default true | Show in public catalog |
| createdAt | Instant | auto | |
| updatedAt | Instant | auto | |
| version | Long | @Version |
- Create DTOs, repository, service:
-
ProductService:- CRUD with tenant isolation (entityId from JWT)
- Catalog query: visible products by category, sorted by sortOrder
- Stock management: decrement on purchase, increment on cancel/return
- Price calculation:
getGrossPrice(),getNetPrice(),getVatAmount()based on vatIncluded flag - Category statistics: count products per category, revenue per category
- Create controller:
- ProductController:
POST /api/products— Create product (CLUB_ADMIN)GET /api/products— List products for entity (admin, paginated, filterable by category/status)GET /api/products/catalog/{entityId}— Public catalog (visible + ACTIVE only, no auth required)GET /api/products/{id}— Get product detailsPUT /api/products/{id}— Update productDELETE /api/products/{id}— Archive product (soft delete: status -> ARCHIVED)
- Create Flyway migration:
V107__create_product.sql- Indexes: (entityId, statusCd), (entityId, categoryCd), (entityId, visible, sortOrder) - Create tests:
ProductServiceTest- Test cases: CRUD, catalog filtering, stock decrement/increment, VAT calculation, capacity check
Result: Product/service management with catalog, stock tracking, and VAT calculation.
Step 1.6 — OpenAPI Documentation
- Add
springdoc-openapi-starter-webmvc-ui - Configure info, security scheme (Bearer JWT),
@Operationannotations - Group by tag (Auth, Member, Contract, Product, Entity)
Result: Swagger UI at /api/swagger-ui.html.
Step 1.7 — Database Schema Validation
- Fresh database, run all Flyway migrations (V000, V001, V100-V107)
- Verify tables, sequences, FKs, indexes
- Insert test data via API, verify integrity
Result: Schema matches Chapter 11.
Step 1.8 — Phase 1 Documentation
- Create
doc/developer/backend-guide.md(project structure, conventions, build/run) - Create
doc/developer/database-guide.md(schema, Flyway, entity patterns) - Create
doc/developer/api-reference.md(auth flow, endpoints, examples)
Step 1.9 — Update Intranet
Run python doc/intranet/build.py.
Phase 1 — Quality Gate
| # | Check | Target |
|---|---|---|
| 1 | Conception document | doc/developer/conception/phase-1-conception.md exists |
| 2 | Compilation | ./mvnw clean compile — 0 errors |
| 3 | Tests | ./mvnw test — 0 failures |
| 4 | Coverage | >= 60% line coverage (JaCoCo) |
| 5 | App starts | /api/health returns 200 |
| 6 | Full flow | Register -> verify -> login -> create member -> purchase membership |
| 7 | Permissions | Unauthenticated -> 401; wrong role -> 403 |
| 8 | OpenAPI | /api/swagger-ui.html shows all endpoints |
| 9 | Security | No hardcoded secrets; JWT RS256; bcrypt; @Version on all entities |
| 10 | Documentation | backend-guide, database-guide, api-reference exist |
| 11 | Intranet | regenerated |
| 12 | CLAUDE.md | updated |
Report: "Phase 1 completed." with entity count, endpoint count, test count.