1
07 CodingStandards
pplate edited this page 2026-06-11 11:41:47 +00:00

CannaManage — Coding Standards & Git Strategy

Phase 4a | Document 7 of 7
Date: 2026-04-06
Stack: Java 21 · Spring Boot 3.x · JPA/Hibernate · PrimeFaces JSF · PostgreSQL


Table of Contents

  1. Project Structure
  2. Java Coding Standards
  3. Compliance Code Rules
  4. Git Strategy
  5. Testing Standards
  6. Code Review Checklist
  7. Security Standards
  8. Environment Configuration

1. Project Structure

Maven Multi-Module Layout

cannamanage/
├── pom.xml                         # Parent POM — dependency management, versions
├── cannamanage-domain/             # JPA entities, enums, exceptions, value objects
│   └── src/main/java/de/cannamanage/domain/
│       ├── member/                 # Member, MemberStatus, MembershipType
│       ├── distribution/           # Distribution, DistributionRecord
│       ├── stock/                  # Strain, Batch, BatchStatus
│       ├── compliance/             # ComplianceConstants, QuotaExceededException
│       └── common/                 # AbstractTenantEntity, TenantId
│
├── cannamanage-service/            # Business logic, compliance engine, repositories
│   └── src/main/java/de/cannamanage/service/
│       ├── member/                 # MemberService, MemberRepository
│       ├── distribution/           # DistributionService, DistributionRepository
│       ├── stock/                  # StockService, BatchRepository
│       ├── compliance/             # ComplianceService, QuotaCalculator
│       └── report/                 # ReportDataService
│
├── cannamanage-web/                # PrimeFaces JSF backing beans + XHTML views
│   └── src/main/
│       ├── java/de/cannamanage/web/
│       │   ├── admin/              # AdminDashboardBean, DistributionFormBean
│       │   ├── member/             # MemberDashboardBean
│       │   └── common/             # AuthBean, NavigationBean
│       └── webapp/
│           ├── admin/              # dashboard.xhtml, distribution-form.xhtml, stock.xhtml
│           ├── member/             # dashboard.xhtml, stock.xhtml
│           └── WEB-INF/            # faces-config.xml, web.xml
│
├── cannamanage-api/                # REST controllers (Spring Boot MVC)
│   └── src/main/java/de/cannamanage/api/
│       ├── member/                 # MemberController, MemberDto
│       ├── distribution/           # DistributionController, DistributionDto
│       ├── stock/                  # StockController, BatchDto
│       ├── auth/                   # AuthController, JwtFilter
│       └── report/                 # ReportController
│
└── cannamanage-report/             # iText 7 PDF generation
    └── src/main/java/de/cannamanage/report/
        ├── monthly/                # MonthlyComplianceReport
        ├── recall/                 # BatchRecallReport
        └── export/                 # MemberCsvExporter

Module Dependencies

cannamanage-domain    (no deps on other modules)
       ↑
cannamanage-service  (depends on domain)
       ↑
cannamanage-api      (depends on service, domain)
cannamanage-web      (depends on service, domain)
cannamanage-report   (depends on service, domain)

cannamanage-api and cannamanage-web are siblings — they do not depend on each other. The web module is the PrimeFaces JSF frontend (MVP); the API module provides the REST layer (future mobile / integration use).


2. Java Coding Standards

Language Version

Java 21. All modern language features are permitted and preferred:

Feature Use Case Example
Records DTOs, value objects, query results record MemberSummary(UUID id, String name, BigDecimal quotaUsed)
Sealed classes Result types, compliance outcomes sealed interface QuotaResult permits QuotaOk, QuotaWarning, QuotaExceeded
Text blocks JPQL, SQL in tests, JSON fixtures String jpql = """ SELECT m FROM Member m WHERE... """
Pattern matching instanceof Type checks in services if (result instanceof QuotaExceeded e) { ... }
Switch expressions Status mapping, report routing yield syntax preferred

Package Structure

Pattern: de.cannamanage.[module].[layer]

de.cannamanage.domain.member          # Member entity
de.cannamanage.domain.compliance      # ComplianceConstants, exceptions
de.cannamanage.service.distribution   # DistributionService
de.cannamanage.api.stock              # StockController, BatchDto
de.cannamanage.web.admin              # DistributionFormBean
de.cannamanage.report.monthly         # MonthlyComplianceReport

Class Naming Conventions

Type Pattern Example
JPA Entity {Domain} Member, Distribution, Batch
Spring Service {Domain}Service MemberService, ComplianceService
Repository {Domain}Repository DistributionRepository
REST Controller {Domain}Controller StockController
JSF Backing Bean {Screen}Bean DistributionFormBean, AdminDashboardBean
DTO (request) {Domain}Request CreateDistributionRequest
DTO (response) {Domain}Response / {Domain}Dto MemberSummaryDto
Exception {Condition}Exception QuotaExceededException, BatchRecalledException
Enum {Domain}Status / {Domain}Type BatchStatus, MembershipType
Constants class {Domain}Constants ComplianceConstants

Dependency Injection

Constructor injection only. Field injection (@Autowired on fields) is prohibited.

// ✅ Correct
@Service
@RequiredArgsConstructor
public class DistributionService {

    private final DistributionRepository distributionRepository;
    private final ComplianceService complianceService;
    private final MemberRepository memberRepository;
}

// ❌ Prohibited
@Service
public class DistributionService {

    @Autowired
    private DistributionRepository distributionRepository;
}

Lombok @RequiredArgsConstructor is the preferred way to generate the constructor.

Entity Base Class

All @Entity classes must extend AbstractTenantEntity. No raw entities without tenant isolation.

// de.cannamanage.domain.common.AbstractTenantEntity
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AbstractTenantEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    @Column(name = "tenant_id", nullable = false, updatable = false)
    private UUID tenantId;

    @CreatedDate
    @Column(name = "created_at", nullable = false, updatable = false)
    private Instant createdAt;

    @LastModifiedDate
    @Column(name = "updated_at", nullable = false)
    private Instant updatedAt;
}

// ✅ All entities extend this
@Entity
@Table(name = "members")
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Member extends AbstractTenantEntity {
    // domain fields only — no id/tenantId/audit fields here
}

Transaction Boundaries

  • @Transactional belongs on service layer methods only
  • Controllers and repositories must not declare @Transactional
  • Use @Transactional(readOnly = true) for query-only methods — improves performance with Hibernate's read-only session optimization
// ✅ Service layer — correct
@Service
@RequiredArgsConstructor
public class MemberService {

    @Transactional(readOnly = true)
    public MemberSummaryDto getById(UUID memberId, UUID tenantId) { ... }

    @Transactional
    public void updateMemberStatus(UUID memberId, MemberStatus status, UUID tenantId) { ... }
}

// ❌ Controller — prohibited
@RestController
public class MemberController {

    @Transactional  // Never here
    @GetMapping("/members/{id}")
    public MemberSummaryDto getMember(@PathVariable UUID id) { ... }
}

Lombok Usage

Annotation Allowed Notes
@Getter On entities and DTOs
@Setter Use sparingly on entities; prefer builder pattern
@Builder On entities and DTOs
@RequiredArgsConstructor Services, beans (for DI)
@NoArgsConstructor JPA requires no-arg constructor
@AllArgsConstructor With @Builder
@ToString Exclude sensitive fields: @ToString.Exclude on passwordHash etc.
@EqualsAndHashCode Entities: only on id field
@Data Prohibited on entities — generates mutable setters for all fields, breaks JPA proxy patterns
@SneakyThrows Never hide checked exceptions

Code Style

  • Checkstyle config: Google Java Style Guide (checkstyle-google.xml in parent POM)
  • Indentation: 4 spaces (no tabs)
  • Line length: 120 characters max
  • No magic numbers — use named constants or enums:
// ❌ Magic number
if (member.getAge() < 21) { limit = 30; }

// ✅ Named constant
if (member.getAge() < ComplianceConstants.AGE_LIMIT_UNDER21) {
    limit = ComplianceConstants.MONTHLY_LIMIT_UNDER21_GRAMS;
}

3. Compliance Code Rules

These rules apply exclusively to code that enforces CanG (Cannabisgesetz) distribution limits. Violations here carry legal risk.

Compliance Constants

All legal limits live in a single, centrally tested constants class. Never hardcode these values inline.

// de.cannamanage.domain.compliance.ComplianceConstants
public final class ComplianceConstants {

    private ComplianceConstants() {} // no instantiation

    /** Maximum grams per single distribution for any member. */
    public static final BigDecimal DAILY_LIMIT_GRAMS = new BigDecimal("25.0");

    /** Monthly gram limit for adult members (age ≥ 21). */
    public static final BigDecimal MONTHLY_LIMIT_ADULT_GRAMS = new BigDecimal("50.0");

    /** Monthly gram limit for members under 21 years of age (CanG §10 Abs.1). */
    public static final BigDecimal MONTHLY_LIMIT_UNDER21_GRAMS = new BigDecimal("30.0");

    /** Age threshold below which the reduced monthly limit applies. */
    public static final int AGE_LIMIT_UNDER21 = 21;

    /** Minimum age for club membership (CanG §15 Abs.1). */
    public static final int MINIMUM_MEMBER_AGE = 18;
}

ComplianceService Rules

  1. ComplianceService methods must always execute within a @Transactional boundary — either by being called from a service method already in a transaction, or by declaring @Transactional themselves. The compliance check and the distribution record creation must be atomic.

  2. Every public method in ComplianceService must have a corresponding test in ComplianceServiceTest that exercises its boundary conditions.

  3. ComplianceService is the only class permitted to read ComplianceConstants limits and make pass/fail decisions. No other class performs limit arithmetic.

@Service
@RequiredArgsConstructor
public class ComplianceService {

    private final DistributionRepository distributionRepository;

    /**
     * Validates whether a distribution of the given weight is permitted for the member.
     *
     * <p>Checks the daily single-distribution limit and the member's monthly quota.
     * Must be called inside an existing @Transactional boundary — the calling
     * DistributionService is responsible for the transaction.
     *
     * @param memberId   the member receiving the distribution
     * @param tenantId   the club's tenant identifier
     * @param weightGrams the proposed distribution weight in grams
     * @return QuotaOk if permitted; QuotaWarning if >80% used; QuotaExceeded if over limit
     * @throws IllegalArgumentException if weightGrams exceeds DAILY_LIMIT_GRAMS
     */
    public QuotaResult checkDistributionAllowed(UUID memberId, UUID tenantId, BigDecimal weightGrams) {
        if (weightGrams.compareTo(ComplianceConstants.DAILY_LIMIT_GRAMS) > 0) {
            throw new IllegalArgumentException(
                "Single distribution exceeds daily limit of " + ComplianceConstants.DAILY_LIMIT_GRAMS + "g");
        }
        // ... monthly quota logic using ComplianceConstants
    }
}

Distribution Record Immutability

Once written, a Distribution record may never be modified (legal audit trail requirement). Enforce this at the JPA level:

@Entity
@Table(name = "distributions")
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Distribution extends AbstractTenantEntity {

    @Column(name = "member_id", nullable = false, updatable = false)
    private UUID memberId;

    @Column(name = "batch_id", nullable = false, updatable = false)
    private UUID batchId;

    @Column(name = "weight_grams", nullable = false, updatable = false,
            precision = 8, scale = 2)
    private BigDecimal weightGrams;

    @Column(name = "distributed_at", nullable = false, updatable = false)
    private Instant distributedAt;

    @Column(name = "recorded_by_admin_id", nullable = false, updatable = false)
    private UUID recordedByAdminId;

    // No setters — @Getter only, no @Setter
    // updatable = false on ALL columns — Hibernate will reject any UPDATE attempt
}

Compliance Test Coverage Requirement

ComplianceServiceTest must include at minimum:

Test Method What It Covers
checkDistributionAllowed_givenWeightAt25g_shouldReturnQuotaOk Exactly at daily limit
checkDistributionAllowed_givenWeightOver25g_shouldThrowIllegalArgument Daily limit exceeded
checkDistributionAllowed_givenMemberAtMonthlyLimit_shouldReturnQuotaExceeded Adult at 50g
checkDistributionAllowed_givenUnder21MemberAt30g_shouldReturnQuotaExceeded Under-21 at 30g
checkDistributionAllowed_givenUnder21MemberAtAdultLimit_shouldReturnQuotaExceeded Under-21 must not reach 50g
checkDistributionAllowed_givenMemberAt80Percent_shouldReturnQuotaWarning Warning threshold
checkDistributionAllowed_givenMemberAt40g_shouldReturnQuotaOk Normal adult, within limit

4. Git Strategy

Branching Model — GitHub Flow (Solo Dev)

main ──────────────────────────────────────────────────────► (production-ready)
       │                  │                    │
       └─► feature/US-042─┘    └─► fix/member-age-edge ─┘
Branch Purpose Merge Via
main Production-ready code only; protected PR only
develop Integration branch for in-progress work Merge to main when stable
feature/US-XXX-short-description New feature tied to a user story PR → develop → main
fix/short-description Bug fix PR → main (or develop if risk is low)
chore/short-description Dependency updates, config, CI PR → main

Branch naming examples:

  • feature/US-042-compliance-quota-check
  • feature/US-015-member-registration-form
  • fix/member-under21-age-boundary
  • chore/update-spring-boot-3.3.1

Commit Message Format — Conventional Commits

type(scope): short description (imperative, ≤72 chars)

[optional body — explain WHY, not WHAT; reference CanG sections if relevant]

[optional footer]
BREAKING CHANGE: description if applicable
Closes #issue-number

Types

Type When to Use
feat New feature or user-visible behavior
fix Bug fix
docs Documentation only
style Formatting, whitespace — no logic change
refactor Code restructuring — no behavior change
test Adding or updating tests
chore Build, deps, config, CI — no production code

Scopes

Scope Module / Area
member Member management
distribution Distribution recording and history
stock Strain and batch management
compliance ComplianceService, ComplianceConstants, CanG limits
auth JWT, Spring Security, login
report PDF/CSV generation
infra Docker, CI, Flyway migrations
web PrimeFaces JSF views and backing beans
api REST controllers and DTOs

Commit Examples

feat(compliance): add daily 25g distribution limit check

Implements CanG §10 Abs.1 single-distribution cap. ComplianceService
now throws IllegalArgumentException before any quota calculation if
weightGrams > ComplianceConstants.DAILY_LIMIT_GRAMS.

fix(member): correct under-21 flag when age is exactly 21

Age comparison was using < instead of <=. Members who turn 21 on the
exact distribution date now correctly receive the adult (50g) limit.
Closes #17

test(distribution): add quota boundary tests for 30g under-21 limit

Adds 6 parameterized test cases covering 28g, 29g, 29.9g, 30g, 30.1g,
and 31g for under-21 members. All reference ComplianceConstants — no
hardcoded values in test assertions.

chore(deps): update Spring Boot to 3.3.1

CVE-2024-38821 fix included. No API changes required.

docs(compliance): document ComplianceConstants usage policy in README

Tag Strategy

Semantic versioning: v{MAJOR}.{MINOR}.{PATCH}

git tag -a v1.0.0 -m "Initial release — core member + distribution management"
git tag -a v1.1.0 -m "Add member portal with quota view"
git tag -a v1.0.1 -m "Fix under-21 monthly limit boundary condition"

5. Testing Standards

Framework Stack

Layer Framework Annotation / Config
Unit tests JUnit 5 + Mockito @ExtendWith(MockitoExtension.class)
Integration tests Spring Boot Test + Testcontainers @SpringBootTest, @Testcontainers
Web layer tests MockMvc @WebMvcTest(DistributionController.class)
Repository tests DataJpaTest + Testcontainers Real PostgreSQL via Testcontainers
PDF generation tests JUnit 5 + iText assertions Verify PDF structure, not pixel comparison

Test Naming Convention

methodName_givenCondition_shouldExpectedBehavior
// ✅ Correct
@Test
void checkDistributionAllowed_givenMemberAtMonthlyLimit_shouldThrowQuotaExceededException()

@Test
void createDistribution_givenValidRequest_shouldPersistAndReturnDto()

@Test
void getQuotaRemaining_givenUnder21Member_shouldCapAt30g()

@Test
void generateMonthlyReport_givenNoPriorDistributions_shouldReturnZeroTotals()

Unit Test Structure

@ExtendWith(MockitoExtension.class)
class ComplianceServiceTest {

    @Mock
    private DistributionRepository distributionRepository;

    @InjectMocks
    private ComplianceService complianceService;

    @Test
    void checkDistributionAllowed_givenMemberAtMonthlyLimit_shouldReturnQuotaExceeded() {
        // GIVEN
        UUID memberId = UUID.randomUUID();
        UUID tenantId = UUID.randomUUID();
        BigDecimal currentMonthTotal = ComplianceConstants.MONTHLY_LIMIT_ADULT_GRAMS;

        when(distributionRepository.sumWeightByMemberAndMonth(eq(memberId), eq(tenantId), any()))
            .thenReturn(currentMonthTotal);

        // WHEN
        QuotaResult result = complianceService.checkDistributionAllowed(
            memberId, tenantId, new BigDecimal("1.0"));

        // THEN
        assertThat(result).isInstanceOf(QuotaExceeded.class);
    }
}

Integration Test Structure

@SpringBootTest
@Testcontainers
@Transactional  // rolls back after each test
class DistributionServiceIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");

    @DynamicPropertySource
    static void configureDataSource(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    // Tests run against real PostgreSQL — Flyway migrations apply automatically
}

Coverage Target

Module Line Coverage Target
cannamanage-service ≥ 80% (enforced by JaCoCo in CI)
cannamanage-domain ≥ 70% (entities + value objects)
cannamanage-api ≥ 70% (controllers via MockMvc)
cannamanage-report ≥ 60% (PDF generation harder to test)
cannamanage-web Best effort (JSF backing beans — limited testability)

Test Rules

  1. No test may hardcode a compliance limit value. All assertions must reference ComplianceConstants:
// ❌ Prohibited
assertThat(limit).isEqualTo(new BigDecimal("50.0"));

// ✅ Required
assertThat(limit).isEqualTo(ComplianceConstants.MONTHLY_LIMIT_ADULT_GRAMS);
  1. Parameterized tests (@ParameterizedTest) are strongly preferred for boundary condition coverage.

  2. Test data builders (or fixtures) must live in src/test/java/.../fixtures/ — no anonymous object creation scattered across test methods.


6. Code Review Checklist

Since CannaManage is a solo development project, a self-review checklist replaces a peer review process. All items must be checked before merging any PR to main.

Self-Review Checklist

## Compliance & Legal
- [ ] All distribution limits reference `ComplianceConstants` — zero hardcoded values
- [ ] `Distribution` entity fields are annotated `@Column(updatable = false)` where required
- [ ] `ComplianceService` calls are only made inside `@Transactional` boundaries
- [ ] New compliance rules have corresponding unit tests in `ComplianceServiceTest`

## Data & Multi-Tenancy
- [ ] New entity extends `AbstractTenantEntity`
- [ ] `tenant_id` is never accepted from user input (HTTP body, query param, path variable)
- [ ] All repository queries filter by `tenantId` — no cross-tenant data leakage possible

## Security & DSGVO
- [ ] No PII in log statements (no email, full name, member number in log lines)
- [ ] No passwords, tokens, or secrets hardcoded anywhere
- [ ] New REST endpoints annotated with `@PreAuthorize`
- [ ] DTOs validated with Bean Validation annotations (`@NotNull`, `@Size`, etc.)

## Database
- [ ] Flyway migration file added for any schema change (`V{n}__description.sql`)
- [ ] Migration file is backward-compatible or includes rollback notes
- [ ] No `@Column(nullable = false)` added without corresponding DB migration

## Code Quality
- [ ] Constructor injection used — no `@Autowired` field injection
- [ ] No `@Data` on JPA entities
- [ ] No magic numbers — named constants or enums used
- [ ] Checkstyle passes locally (`./mvnw checkstyle:check`)
- [ ] Javadoc on all public service methods

## Testing
- [ ] Unit test added for new service method
- [ ] Integration test updated if schema or contract changed
- [ ] Test coverage does not decrease in `cannamanage-service`
- [ ] Test method names follow `method_givenCondition_shouldExpect` pattern

## General
- [ ] Commit message follows Conventional Commits format
- [ ] Branch name follows `feature/US-XXX-` or `fix/` convention
- [ ] No `TODO` comments left in production code (use GitHub Issues instead)

7. Security Standards

Authentication & Authorization

// JWT secret from environment only — never in application.properties
@Value("${JWT_SECRET}")
private String jwtSecret;

// All endpoints behind @PreAuthorize — no security by obscurity
@RestController
@RequestMapping("/api/v1/distributions")
public class DistributionController {

    @GetMapping
    @PreAuthorize("hasRole('ADMIN')")
    public Page<DistributionDto> list(...) { ... }

    @PostMapping
    @PreAuthorize("hasRole('ADMIN')")
    public DistributionDto create(...) { ... }
}

// Member portal endpoints restricted to role + own data
@GetMapping("/api/v1/member/quota")
@PreAuthorize("hasRole('MEMBER') and #memberId == authentication.principal.memberId")
public QuotaDto getQuota(@RequestParam UUID memberId) { ... }

CORS Configuration

@Bean
CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    // No wildcard — club subdomain only
    config.setAllowedOriginPatterns(List.of("https://*.cannamanage.de"));
    config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
    config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
    config.setAllowCredentials(true);
    // ...
}

Input Validation

All DTOs must be annotated with Bean Validation constraints. The controller calls @Valid on request bodies.

public record CreateDistributionRequest(

    @NotNull(message = "Member ID is required")
    UUID memberId,

    @NotNull(message = "Batch ID is required")
    UUID batchId,

    @NotNull(message = "Weight is required")
    @DecimalMin(value = "0.1", message = "Weight must be at least 0.1g")
    @DecimalMax(value = "25.0", message = "Weight cannot exceed daily limit")
    BigDecimal weightGrams
) {}

SQL Injection Prevention

  • JPA named queries only — no string concatenation in JPQL
  • Spring Data JPA repository methods generate parameterized queries automatically
  • Native SQL queries use @Query with named parameters (:param syntax), never +
// ✅ Safe — parameterized
@Query("SELECT SUM(d.weightGrams) FROM Distribution d WHERE d.memberId = :memberId AND d.tenantId = :tenantId AND MONTH(d.distributedAt) = :month")
BigDecimal sumWeightByMemberAndMonth(@Param("memberId") UUID memberId,
                                     @Param("tenantId") UUID tenantId,
                                     @Param("month") int month);

// ❌ Prohibited — SQL injection risk
String jpql = "SELECT ... WHERE name = '" + memberName + "'";

Password Hashing

@Bean
PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(12);  // strength 12 — ~250ms per hash on modern hardware
}

Sensitive Data Logging

// ❌ Never log PII
log.info("Processing distribution for member: {}", member.getEmail());
log.info("Member {} requested quota", member.getFullName());

// ✅ Log with opaque identifiers only
log.info("Processing distribution for memberId={} tenantId={}", member.getId(), tenantId);
log.info("Quota check passed for memberId={}", memberId);

8. Environment Configuration

Environment Variables Reference

All secrets and environment-specific configuration are provided via environment variables. Never commit secrets to version control.

Variable Required Default Description
DB_URL JDBC URL, e.g. jdbc:postgresql://localhost:5432/cannamanage
DB_USERNAME PostgreSQL username
DB_PASSWORD PostgreSQL password
JWT_SECRET 256-bit (32-byte) random secret for JWT signing; generate with openssl rand -base64 32
JWT_ACCESS_TTL_HOURS 8 Access token TTL in hours
JWT_REFRESH_TTL_DAYS 30 Refresh token TTL in days
STRIPE_SECRET_KEY (billing) Stripe secret key (starts with sk_live_ in production)
STRIPE_WEBHOOK_SECRET (billing) Stripe webhook signing secret for subscription events
MAIL_HOST SMTP host for transactional emails
MAIL_USERNAME SMTP username
MAIL_PASSWORD SMTP password
MAIL_FROM noreply@cannamanage.de From address for system emails
SENTRY_DSN Sentry DSN for error tracking; omit to disable
APP_BASE_URL Application base URL, e.g. https://meinclub.cannamanage.de
ADMIN_INITIAL_EMAIL Seed admin email on first startup (Flyway data migration)
ADMIN_INITIAL_PASSWORD Seed admin password — change immediately after first login

application.properties Pattern

# application.properties — references env vars only; no values hardcoded

spring.datasource.url=${DB_URL}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}

spring.jpa.hibernate.ddl-auto=validate
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migration

jwt.secret=${JWT_SECRET}
jwt.access.ttl-hours=${JWT_ACCESS_TTL_HOURS:8}
jwt.refresh.ttl-days=${JWT_REFRESH_TTL_DAYS:30}

stripe.secret-key=${STRIPE_SECRET_KEY}
stripe.webhook-secret=${STRIPE_WEBHOOK_SECRET}

spring.mail.host=${MAIL_HOST}
spring.mail.username=${MAIL_USERNAME}
spring.mail.password=${MAIL_PASSWORD}

sentry.dsn=${SENTRY_DSN:}

Profile Strategy

spring.profiles.active=prod is NOT a security mechanism. Never use profile-based condition checks to gate security-relevant behavior (e.g., @ConditionalOnProperty(name="spring.profiles.active", havingValue="prod")).

Profiles are used only for infrastructure wiring (in-memory H2 vs. real PostgreSQL for tests, Testcontainers vs. external DB).

Profile Usage
(none) Production — all config from environment variables
test JUnit integration tests — Testcontainers PostgreSQL
dev Local development — Docker Compose PostgreSQL, verbose SQL logging

Local Development Setup

# Start local PostgreSQL via Docker Compose
docker compose up -d postgres

# Run with dev profile (verbose SQL, local DB)
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev \
  -Dspring-boot.run.arguments="--DB_URL=jdbc:postgresql://localhost:5432/cannamanage_dev \
    --DB_USERNAME=cannamanage --DB_PASSWORD=dev_password \
    --JWT_SECRET=$(openssl rand -base64 32)"

End of CannaManage coding standards. See also 03-ARCHITECTURE.md for data model and 05-API-SPEC.md for REST contract.