Table of Contents
- CannaManage — Coding Standards & Git Strategy
- Table of Contents
- 1. Project Structure
- 2. Java Coding Standards
- Language Version
- Package Structure
- Class Naming Conventions
- Dependency Injection
- Entity Base Class
- Transaction Boundaries
- Lombok Usage
- Code Style
- 3. Compliance Code Rules
- Compliance Constants
- ComplianceService Rules
- Distribution Record Immutability
- Compliance Test Coverage Requirement
- 4. Git Strategy
- 5. Testing Standards
- Framework Stack
- Test Naming Convention
- Unit Test Structure
- Integration Test Structure
- Coverage Target
- Test Rules
- 6. Code Review Checklist
- 7. Security Standards
- Authentication & Authorization
- CORS Configuration
- Input Validation
- SQL Injection Prevention
- Password Hashing
- Sensitive Data Logging
- 8. Environment Configuration
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
- Project Structure
- Java Coding Standards
- Compliance Code Rules
- Git Strategy
- Testing Standards
- Code Review Checklist
- Security Standards
- 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
@Transactionalbelongs 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.xmlin 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
-
ComplianceServicemethods must always execute within a@Transactionalboundary — either by being called from a service method already in a transaction, or by declaring@Transactionalthemselves. The compliance check and the distribution record creation must be atomic. -
Every public method in
ComplianceServicemust have a corresponding test inComplianceServiceTestthat exercises its boundary conditions. -
ComplianceServiceis the only class permitted to readComplianceConstantslimits 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-checkfeature/US-015-member-registration-formfix/member-under21-age-boundarychore/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
- 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);
-
Parameterized tests (
@ParameterizedTest) are strongly preferred for boundary condition coverage. -
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
@Querywith named parameters (:paramsyntax), 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=prodis 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.
🌿 CannaManage
📋 Planning
🏗️ Architecture
🎨 Design
💻 Development
🌟 Product
📊 Sprint Status
| Sprint | Theme | Status |
|---|---|---|
| 1 | Domain Foundation | ✅ |
| 2 | REST API | ✅ |
| 3 | Staff & Portal | ✅ |
| 4 | Frontend MVP | ✅ |
| 5 | API Integration | ✅ |
| 6 | Production Readiness | ✅ |
| 7 | Communication | ✅ |
| 8 | Vereinsverwaltung | ✅ |
| 9 | Berichtszentrale | ✅ |
| 10 | Payment Import | ✅ |
| 11 | Test Coverage | ✅ |
| 12 | Golden Tests | ✅ |
| 13 | Prod Hardening | ✅ |
| 14 | Marketing | ✅ |
📈 Metrics
| Metric | Value |
|---|---|
| Entities | 57 |
| Controllers | 33 |
| Migrations | V1–V36 |
| Tests | 500+ |
| Coverage | 80% |