Appearance
Development Conventions
Coding standards and patterns extracted from the CLAUDE.md files in the backend and frontend repositories.
Backend (../schemastack-be)
Naming
| Context | Convention | Example |
|---|---|---|
| Java classes | PascalCase | ViewColumnService |
| Java methods/variables | camelCase | findByUuid |
| Database tables/columns | snake_case | view_column |
| REST endpoints | kebab-case | /api/workspace-members |
| Documentation files | lowercase-with-dashes | system-architecture.md |
API Design — UUIDs Only
- REST API responses must use UUIDs, never internal numeric IDs (
Long id) - DTOs exposed to the frontend have a
uuidfield (UUID), not anidfield (Long) - Internal numeric IDs are implementation details that must never leak to the frontend
- Path parameters:
/api/constraints/{constraintUuid} - Request bodies reference related entities by UUID:
"columnUuid": "a1b2c3d4-..."
ViewColumn Architecture
ViewColumn uses composition, not inheritance:
java
@Entity
@Table(name = "view_column")
public class ViewColumn extends PanacheEntity {
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "column_id", nullable = false)
private ColumnMetadata column; // HAS-A relationship, not IS-A
}- Schema import: ColumnMetadata is created FIRST, then ViewColumn references it
- ViewColumn properties: UI-specific (displayName, widgetType, visible, sortable, readonly)
- ColumnMetadata properties: Database column properties (name, type, nullable, unique, length)
- Delegating getters on ViewColumn forward to
column.getXxx()for backwards compatibility
Reactive Programming Rules
- All database operations use Hibernate Reactive (Mutiny)
- Return
Uni<T>for single results,Multi<T>for streams - Use
@WithTransactionfor transactional methods - Never fetch lazy collections in parallel — causes session corruption. Use sequential chains or JOIN FETCH
- See Patterns & Known Issues for HR000069 solutions
Nested Session Prevention
The #1 cause of HR000069 errors is nested sessions:
- Service manages the entire lifecycle — endpoint just calls the service, doesn't open sessions
- Use JOIN FETCH to eagerly load ALL associations
- Extract entity data to primitives before thread switching
.call()before.flatMap()forces session to closePanache.withSession()ends before the.flatMap()with thread switch.runSubscriptionOn()only runs after session is closed
JBoss Logger — Avoid Method Ambiguity
JBoss Logger has overloading ambiguity with wrapper types (Long, Integer):
java
// BAD — ambiguous
LOG.debugf("Processing entity %d with %d fields", entityId, data.size());
// GOOD — use %s for wrapper types
LOG.debugf("Processing entity %s with %s fields", entityId, data.size());When mixing wrapper types with primitives, cast the wrapper to Object:
java
LOG.debugf("Entity %s with %s fields", (Object) entityId, data.size());Database Migrations
- Always use Liquibase for schema changes in Quarkus modules
- Changesets must be idempotent and reversible
- Never modify existing changesets — create new ones
- Version format:
changeset-{module}-{version}-{description}.xml
Message Contracts
- All RabbitMQ messages defined in
messaging-contractsmodule - Must be framework-agnostic (no Quarkus/Spring dependencies)
- Use Jackson for JSON serialization
- Include builder pattern with Lombok
Exception Handling in Reactive Chains
Let JAX-RS exceptions (WebApplicationException, ForbiddenException, NotFoundException) bubble up to exception mappers. Use predicates to exclude them:
java
return service.doSomething()
.onFailure(NotFoundException.class).recoverWithItem(...)
.onFailure(t -> !(t instanceof WebApplicationException)).recoverWithItem(...);Running Tests
Maven skips tests by default. Always pass -DskipTests=false:
bash
cd quarkus/metadata/metadata-test
../../../mvnw test -DskipTests=false
# Specific test
../../../mvnw test -Dtest=SchemaDuplicateColumnTest -DskipTests=falseModule Dependencies
- Organisation module is intentionally standalone and reusable — minimal dependencies on metadata
- Organisation → can be extracted for future SaaS projects
- Metadata → CAN depend on organisation (users, orgs, workspaces, permissions)
- No circular dependencies between modules
Frontend (../schemastack-fe)
Critical Rules
- Flag backend contract mismatches rather than hacking the frontend to work around them. The app is pre-launch so the backend should be fixed.
- No component-level style encapsulation — ALL styles go in
src/styles/folder, never usestyleUrlorstylesin component decorators. Scope by prefixing selectors with component tag names.
Optimistic Updates with SSE Pattern
All CRUD operations that can be synced with other users follow this pattern:
- Component dispatches BOTH local and API actions
- Reducer handles local action immediately (optimistic update)
- Effect handles API call and returns success/failure
- SSE broadcasts update to other users (filtered by userId)
- Other clients receive SSE event and apply update
Critical rules:
- Always get state BEFORE local reorder using
take(1) - Calculate position from original state, not after the local reorder
- Dispatch local action first (instant feedback), then API action (persistence)
- Use
exhaustMapin effects when actions can fire rapidly - Always filter SSE by userId to prevent echo loops
- Positions are 0-based
Sidenav Pages — Prevent Layout Shift
Pages with sidenav MUST call sidenavService.setMenuItems() in the constructor, NOT in ngOnInit(). Setting items in ngOnInit() causes visible flicker as the template conditionally renders the sidenav container.
Color Styling Policy
- Never use Tailwind for colors (no
bg-blue-500,text-red-600) - Always use Material Design 3 tokens via
mat.theme-overrides()and component override mixins - Custom utility classes (
.bg-primary,.text-muted-foreground) map to Material tokens - This ensures a single color system with automatic theme switching
Typography Policy
- Never use inline
style=""for font properties - Check M3 typography classes first (
.mat-label-small,.mat-body-medium,.mat-title-small, etc.) - Custom type scale lives in
shared/styles/_typography.scss - For
font-weight: 600, use.font-semibold(M3 only provides 400/500/700) - For mat-icon sizing, use
.icon-sm/.icon-md/.icon-lg/.icon-xl
M3 Styling Priority System
Follow this exact priority order when styling Material components:
| Priority | Method | When to Use |
|---|---|---|
| 1 | mat.theme() | Initial project setup — global color scheme and typography |
| 2 | mat.theme-overrides() | System-level token overrides affecting all components |
| 2.5 | Component override mixins | Fine-grained per-component-type control (mat.card-overrides(), mat.button-overrides(), etc.) |
| 3 | --mat-component-* variables | Component-specific CSS variable overrides |
| 4 | Tailwind classes | Layout and structural styling only (flex, grid, spacing, positioning) |
| 5 | Custom SCSS/CSS | Last resort — unique animations, filters, complex combinations |
Finding the right variable: DevTools → Inspect element → Computed tab → filter --mat-.
Reusability-First Styling
Before writing new styles:
- Check
src/styles/_utilities.scssfor existing utilities - If it exists, use it
- If not, determine if it's reusable (used in 2+ places?) → create utility in
_utilities.scss - Only truly component-unique styles go in
src/styles/components/ - Use semantic naming (
.status-badgenot.workspace-member-status)
Path Aliases
Use @app/* to reference files from src/app/:
typescript
import { AuthService } from '@app/auth/services/auth.service';
import { AppState } from '@app/store';HTTP Interceptors (order matters)
authInterceptorFn— adds JWT tokenloggingInterceptorFn— logs requests/responsesloadingBarInterceptorFn— controls loading bar UIloadingInterceptor— manages global loading state
Proxy Configuration
/api→http://localhost:8080(Quarkus backend)/sse→http://localhost:8081(SSE events)