Migrate project artifacts to spec-kit format

- Move cross-cutting docs (personas, design system, implementation phases,
  Ideen.md) to .specify/memory/
- Move cross-cutting research and plans to .specify/memory/research/ and
  .specify/memory/plans/
- Extract 5 setup tasks from spec/setup-tasks.md into individual
  specs/001-005/spec.md files with spec-kit template format
- Extract 20 user stories from spec/userstories.md into individual
  specs/006-026/spec.md files with spec-kit template format
- Relocate feature-specific research and plan docs into specs/[feature]/
- Add spec-kit constitution, templates, scripts, and slash commands
- Slim down CLAUDE.md to Claude-Code-specific config, delegate principles
  to .specify/memory/constitution.md
- Update ralph.sh with stream-json output and per-iteration logging
- Delete old spec/ and docs/agents/ directories
- Gitignore Ralph iteration JSONL logs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 20:19:41 +01:00
parent 0b2b84dafc
commit 6aeb4b8bca
83 changed files with 6486 additions and 660 deletions

View File

@@ -0,0 +1,322 @@
# Research Report: API-First Approach
**Date:** 2026-03-04
**Scope:** API-first development with Spring Boot backend and Vue 3 frontend
**Status:** Complete
## Context
The fete project needs a strategy for API design and implementation. Two fundamental approaches exist:
- **Code-first:** Write annotated Java controllers, generate OpenAPI spec from code (e.g., springdoc-openapi)
- **API-first (spec-first):** Write OpenAPI spec as YAML, generate server interfaces and client types from it
This report evaluates API-first for the fete stack (Spring Boot 3.5.x, Java 25, Vue 3, TypeScript).
## Why API-First
| Aspect | Code-First | API-First |
|--------|-----------|-----------|
| Source of truth | Java source code | OpenAPI YAML file |
| Parallel development | Backend must exist first | Frontend + backend from day one |
| Contract stability | Implicit, can drift | Explicit, version-controlled, reviewed |
| Spec review in PRs | Derived artifact | First-class reviewable diff |
| Runtime dependency | springdoc library at runtime | None (build-time only) |
| Hexagonal fit | Controllers define contract | Spec defines contract, controllers implement |
API-first aligns with the project statutes:
- **No vibe coding**: the spec forces deliberate API design before implementation.
- **Research → Spec → Test → Implement**: the OpenAPI spec IS the specification for the API layer.
- **Privacy**: no runtime documentation library needed (no springdoc serving endpoints).
- **KISS**: one YAML file is the single source of truth for both sides.
## Backend: openapi-generator-maven-plugin
### Tool Assessment
- **Project:** [OpenAPITools/openapi-generator](https://github.com/OpenAPITools/openapi-generator)
- **Current version:** 7.20.0 (released 2026-02-16)
- **GitHub stars:** ~22k
- **License:** Apache 2.0
- **Maintenance:** Active, frequent releases (monthly cadence)
- **Spring Boot 3.5.x compatibility:** Confirmed via `useSpringBoot3: true` (Jakarta EE namespace)
- **Java 25 compatibility:** No blocking issues reported for Java 21+
### Generator: `spring` with `interfaceOnly: true`
The `spring` generator offers two modes:
1. **`interfaceOnly: true`** — generates API interfaces and model classes only. You write controllers that implement the interfaces.
2. **`delegatePattern: true`** — generates controllers + delegate interfaces. You implement the delegates.
**Recommendation: `interfaceOnly: true`** — cleaner integration with hexagonal architecture. The generated interface is the port definition, the controller is the driving adapter.
### What Gets Generated
From a spec like:
```yaml
paths:
/events:
post:
operationId: createEvent
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/CreateEventRequest'
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/EventResponse'
```
The generator produces:
- `EventsApi.java` — interface with `@RequestMapping` annotations
- `CreateEventRequest.java` — POJO with Jackson annotations + Bean Validation
- `EventResponse.java` — POJO with Jackson annotations
You then write:
```java
@RestController
public class EventController implements EventsApi {
private final CreateEventUseCase createEventUseCase;
@Override
public ResponseEntity<EventResponse> createEvent(CreateEventRequest request) {
// Map DTO → domain command
// Call use case
// Map domain result → DTO
}
}
```
### Recommended Plugin Configuration
```xml
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>7.20.0</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/src/main/resources/openapi/api.yaml</inputSpec>
<generatorName>spring</generatorName>
<apiPackage>de.fete.adapter.in.web.api</apiPackage>
<modelPackage>de.fete.adapter.in.web.model</modelPackage>
<generateSupportingFiles>true</generateSupportingFiles>
<supportingFilesToGenerate>ApiUtil.java</supportingFilesToGenerate>
<configOptions>
<interfaceOnly>true</interfaceOnly>
<useSpringBoot3>true</useSpringBoot3>
<useBeanValidation>true</useBeanValidation>
<performBeanValidation>true</performBeanValidation>
<openApiNullable>false</openApiNullable>
<skipDefaultInterface>true</skipDefaultInterface>
<useResponseEntity>true</useResponseEntity>
<documentationProvider>none</documentationProvider>
<annotationLibrary>none</annotationLibrary>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
```
Key options rationale:
| Option | Value | Why |
|--------|-------|-----|
| `interfaceOnly` | `true` | Only interfaces + models; controllers are yours |
| `useSpringBoot3` | `true` | Jakarta EE namespace (required for Spring Boot 3.x) |
| `useBeanValidation` | `true` | `@Valid`, `@NotNull` on parameters |
| `openApiNullable` | `false` | Avoids `jackson-databind-nullable` dependency |
| `skipDefaultInterface` | `true` | No default method stubs — forces full implementation |
| `documentationProvider` | `none` | No Swagger UI / springdoc annotations |
| `annotationLibrary` | `none` | Minimal annotations on generated code |
### Build Integration
- Runs in Maven's `generate-sources` phase (before compilation)
- Output: `target/generated-sources/openapi/` — already gitignored
- `mvn clean compile` always regenerates from spec
- No generated code in git — the spec is the source of truth
### Additional Dependencies
With `openApiNullable: false` and `annotationLibrary: none`, minimal additional dependencies are needed. `jakarta.validation-api` is already transitively provided by `spring-boot-starter-web`.
### Hexagonal Architecture Mapping
```
adapter.in.web/
├── api/ ← generated interfaces (EventsApi.java)
├── model/ ← generated DTOs (CreateEventRequest.java, EventResponse.java)
└── controller/ ← your implementations (EventController implements EventsApi)
application.port.in/
└── CreateEventUseCase.java
domain.model/
└── Event.java ← clean domain object (can be a record)
```
Rules:
1. Generated DTOs exist ONLY in `adapter.in.web.model`
2. Domain objects are never exposed to the web layer
3. Controllers map between generated DTOs and domain objects
4. Mapping is manual (project is small enough; no MapStruct needed)
## Frontend: openapi-typescript + openapi-fetch
### Tool Comparison
| Tool | npm Weekly DL | Approach | Runtime | Active |
|------|--------------|----------|---------|--------|
| **openapi-typescript** | ~2.5M | Types only (.d.ts) | 0 kb | Yes |
| **openapi-fetch** | ~1.2M | Type-safe fetch wrapper | 6 kb | Yes |
| orval | ~828k | Full client codegen | Varies | Yes |
| @hey-api/openapi-ts | ~200-400k | Full client codegen | Varies | Yes (volatile API) |
| openapi-generator TS | ~500k | Full codegen (Java needed) | Heavy | Yes |
| swagger-typescript-api | ~43 | Full codegen | Varies | Declining |
### Recommendation: openapi-typescript + openapi-fetch
**Why this combination wins for fete:**
1. **Minimal footprint.** Types-only generation = zero generated runtime code. The `.d.ts` file disappears after TypeScript compilation.
2. **No Axios.** Uses native `fetch` — no unnecessary dependency.
3. **No phone home, no CDN.** Pure TypeScript types + a 6 kb fetch wrapper.
4. **Vue 3 Composition API fit.** Composables wrap `api.GET()`/`api.POST()` calls naturally.
5. **Actively maintained.** High download counts, regular releases, OpenAPI 3.0 + 3.1 support.
6. **Compile-time safety.** Wrong paths, missing parameters, wrong body types = TypeScript errors.
**Why NOT the alternatives:**
- **orval / hey-api:** Generate full runtime code (functions, classes). More than needed. Additional abstraction layer.
- **openapi-generator TypeScript:** Requires Java for generation. Produces verbose classes. Heavyweight.
- **swagger-typescript-api:** Declining maintenance. Not recommended for new projects.
### How It Works
#### Step 1: Generate Types
```bash
npx openapi-typescript ../backend/src/main/resources/openapi/api.yaml -o src/api/schema.d.ts
```
Produces a `.d.ts` file with `paths` and `components` interfaces that mirror the OpenAPI spec exactly.
#### Step 2: Create Client
```typescript
// src/api/client.ts
import createClient from "openapi-fetch";
import type { paths } from "./schema";
export const api = createClient<paths>({ baseUrl: "/api" });
```
#### Step 3: Use in Composables
```typescript
// src/composables/useEvent.ts
import { ref } from "vue";
import { api } from "@/api/client";
export function useEvent(eventId: string) {
const event = ref(null);
const error = ref(null);
async function load() {
const { data, error: err } = await api.GET("/events/{eventId}", {
params: { path: { eventId } },
});
if (err) error.value = err;
else event.value = data;
}
return { event, error, load };
}
```
Type safety guarantees:
- Path must exist in spec → TypeScript error if not
- Path parameters enforced → TypeScript error if missing
- Request body must match schema → TypeScript error if wrong
- Response `data` is typed as the 2xx response schema
### Build Integration
```json
{
"scripts": {
"generate:api": "openapi-typescript ../backend/src/main/resources/openapi/api.yaml -o src/api/schema.d.ts",
"dev": "npm run generate:api && vite",
"build": "npm run generate:api && vue-tsc && vite build"
}
}
```
The generated `schema.d.ts` can be committed to git (it is a stable, deterministic output) or gitignored and regenerated on each build. For simplicity, committing it is pragmatic — it allows IDE support without running the generator first.
### Dependencies
```json
{
"devDependencies": {
"openapi-typescript": "^7.x"
},
"dependencies": {
"openapi-fetch": "^0.13.x"
}
}
```
Requirements: Node.js 20+, TypeScript 5.x, `"module": "ESNext"` + `"moduleResolution": "Bundler"` in tsconfig.
## End-to-End Workflow
```
1. WRITE/EDIT SPEC
backend/src/main/resources/openapi/api.yaml
├──── 2. BACKEND: mvnw compile
│ → target/generated-sources/openapi/
│ ├── de/fete/adapter/in/web/api/EventsApi.java
│ └── de/fete/adapter/in/web/model/*.java
│ → Compiler errors show what controllers need updating
└──── 3. FRONTEND: npm run generate:api
→ frontend/src/api/schema.d.ts
→ TypeScript errors show what composables/views need updating
```
On spec change, both sides get compile-time feedback. The spec is a **compile-time contract**.
## Open Questions
1. **Spec location sharing.** The spec lives in `backend/src/main/resources/openapi/`. The frontend references it via relative path (`../backend/...`). This works in a monorepo. Alternative: symlink or copy step. Relative path is simplest.
2. **Generated `schema.d.ts` — commit or gitignore?** Committing is pragmatic (IDE support without running generator). Gitignoring is purist (derived artifact). Recommend: commit it, regenerate during build to catch drift.
3. **Spec validation in CI.** The openapi-generator-maven-plugin validates the spec during build. Frontend side could add `openapi-typescript` as a build step. Both fail on invalid specs.
## Conclusion
API-first with `openapi-generator-maven-plugin` (backend) and `openapi-typescript` + `openapi-fetch` (frontend) is a strong fit for fete:
- Single source of truth (one YAML file)
- Compile-time contract enforcement on both sides
- Minimal dependencies (no Swagger UI, no Axios, no runtime codegen libraries)
- Clean hexagonal architecture integration
- Actively maintained, well-adopted tooling

View File

@@ -0,0 +1,216 @@
---
date: 2026-03-04T01:40:21+01:00
git_commit: a55174b32333d0f46a55d94a50604344d1ba33f6
branch: master
topic: "Backpressure for Agentic Coding"
tags: [research, backpressure, agentic-coding, quality, tooling, hooks, static-analysis, archunit]
status: complete
---
# Research: Backpressure for Agentic Coding
## Research Question
What tools, methodologies, and patterns exist for implementing backpressure in agentic coding workflows? Which are applicable to the fete tech stack (Java 25, Spring Boot 3.5, Maven, Vue 3, TypeScript, Vitest)?
## Summary
Backpressure in agentic coding means: **automated feedback mechanisms that reject wrong output deterministically**, forcing the agent to self-correct before a human ever sees the result. The concept is borrowed from distributed systems (reactive streams, flow control) and applied to AI-assisted development.
The key insight from the literature: **90% deterministic, 10% agentic.** Encode constraints in the type system, linting rules, architecture tests, and test suites — not in prose instructions. The agent runs verification on its own output, sees failures, and fixes itself. Humans review only code that has already passed all automated gates.
### Core Sources
| Source | Author | Key Contribution |
|--------|--------|-----------------|
| [Don't waste your back pressure](https://ghuntley.com/pressure/) | Geoffrey Huntley | Coined "backpressure for agents." Feedback-driven quality, progressive delegation. |
| [If you don't engineer backpressure, you'll get slopped](https://jw.hn/engineering-backpressure) | JW | Verification hierarchy: types → linting → tests → agentic review. 90/10 rule. |
| [Context-Efficient Backpressure for Coding Agents](https://www.hlyr.dev/blog/context-efficient-backpressure) | HumanLayer | Output filtering, fail-fast, context window preservation. |
| [Claude Code Hooks Reference](https://code.claude.com/docs/en/hooks) | Anthropic | PostToolUse hooks for automated feedback after file edits. |
| [ArchUnit](https://www.archunit.org/) | TNG Technology Consulting | Architecture rules as unit tests. Hexagonal architecture enforcement. |
## Detailed Findings
### 1. The Backpressure Concept
In distributed systems, backpressure prevents upstream producers from overwhelming downstream consumers. Applied to agentic coding:
- **Producer:** The AI agent generating code
- **Consumer:** The quality gates (compiler, linter, tests, architecture rules)
- **Backpressure:** Automated rejection of output that doesn't pass gates
Geoffrey Huntley: *"If you aren't capturing your back-pressure then you are failing as a software engineer."*
The paradigm shift: instead of telling the agent what to do (prompt engineering), **engineer an environment where wrong outputs get rejected automatically** (backpressure engineering).
### 2. The Verification Hierarchy
JW's article establishes a strict ordering — deterministic first, agentic last:
```
Layer 1: Type System (hardest constraint, compile-time)
Layer 2: Static Analysis (linting rules, pattern enforcement)
Layer 3: Architecture Tests (dependency rules, layer violations)
Layer 4: Unit/Integration Tests (behavioral correctness)
Layer 5: Agentic Review (judgment calls — only after 1-4 pass)
```
**Critical rule:** If a constraint can be checked deterministically, it MUST be checked deterministically. Relying on agentic review for things a linter could catch is "building on sand."
**Context efficiency:** Don't dump rules into CLAUDE.md that could be expressed as type constraints, lint rules, or tests. Reserve documentation for architectural intent and domain knowledge that genuinely requires natural language.
### 3. Context-Efficient Output
HumanLayer's research on context window management for coding agents:
- **On success:** Show only `✓` — don't waste tokens on 200 lines of passing test output
- **On failure:** Show the full error — the agent needs the details to self-correct
- **Fail-fast:** Enable `--bail` / `-x` / `-failfast` — one failure at a time prevents context-switching between multiple bugs
- **Filter output:** Strip generic stack frames, timing info, and irrelevant details
**Anti-pattern:** Piping output to `/dev/null` or using `head -n 50` — this hides information the agent might need and can force repeated test runs.
### 4. Claude Code Hooks
Hooks are shell commands that execute automatically at specific points in Claude Code's lifecycle:
| Event | Trigger | Use Case |
|-------|---------|----------|
| `PreToolUse` | Before a tool runs | Block dangerous operations |
| `PostToolUse` | After a tool completes | Run compile/lint/test checks |
| `Stop` | Agent finishes response | Final validation |
| `UserPromptSubmit` | User sends a prompt | Inject context |
| `SessionStart` | Session begins | Setup checks |
**PostToolUse** is the primary backpressure mechanism: after every file edit, run deterministic checks and feed the result back to the agent.
**Configuration:** `.claude/settings.json` (project-level, committed) or `.claude/settings.local.json` (personal, gitignored).
**Hook format example:**
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit:*.java",
"hooks": [
{
"type": "command",
"command": "cd backend && ./mvnw compile -q 2>&1 || true"
}
]
}
]
}
}
```
The hook output is fed back to the agent as context, enabling self-correction in the same conversation turn.
### 5. Applicable Tools for fete's Tech Stack
#### 5.1 Java / Maven Backend
**Checkstyle** (coding conventions)
- Maven plugin: `maven-checkstyle-plugin`
- Enforces formatting, naming, imports, Javadoc rules
- Rulesets: Google Style (most widely adopted), Sun Style (legacy)
- Fails build on violation when configured with `<failOnViolation>true</failOnViolation>`
- Actively maintained, open source (LGPL-2.1)
**SpotBugs** (bug detection)
- Maven plugin: `spotbugs-maven-plugin`
- Successor to FindBugs — finds null pointer dereferences, infinite loops, resource leaks, concurrency bugs
- Runs bytecode analysis (requires compilation first)
- Configurable effort/threshold levels
- Actively maintained, open source (LGPL-2.1)
**Error Prone** (compile-time bug detection)
- Google's javac plugin — catches errors during compilation
- Tighter feedback loop than SpotBugs (compile-time vs. post-compile)
- Requires `maven-compiler-plugin` configuration with annotation processor
- More invasive setup, Java version compatibility can lag
- Actively maintained, open source (Apache-2.0)
**ArchUnit** (architecture enforcement)
- Library for writing architecture rules as JUnit tests
- Built-in support for onion/hexagonal architecture via `onionArchitecture()`
- Dedicated hexagonal ruleset: [archunit-hexagonal](https://github.com/whiskeysierra/archunit-hexagonal)
- Rules: "domain must not depend on adapters", "ports are interfaces", "no Spring annotations in domain"
- Fails as a normal test — agent sees the failure and can fix it
- Actively maintained, open source (Apache-2.0)
#### 5.2 Vue 3 / TypeScript Frontend
**TypeScript strict mode** (already configured)
- `strict: true` via `@vue/tsconfig`
- `noUncheckedIndexedAccess: true` (already in `tsconfig.app.json`)
- `vue-tsc --build` for type-checking (already in `package.json` as `type-check`)
**ESLint + oxlint** (already configured)
- ESLint with `@vue/eslint-config-typescript` (recommended rules)
- oxlint as fast pre-pass (Rust-based, handles simple rules)
- Custom ESLint rules can encode repeated agent mistakes
**Vitest** (already configured)
- `--bail` flag available for fail-fast behavior
- `--reporter=verbose` for detailed output on failure
### 6. Current State Analysis (fete project)
| Layer | Backend | Frontend |
|-------|---------|----------|
| Type System | Java 25 (strong, but no extra strictness configured) | TypeScript strict + `noUncheckedIndexedAccess` ✓ |
| Static Analysis | **Nothing configured** | ESLint + oxlint + Prettier ✓ |
| Architecture Tests | **Nothing configured** | N/A (flat structure) |
| Unit Tests | JUnit 5 via `./mvnw test` ✓ | Vitest via `npm run test:unit` ✓ |
| Claude Code Hooks | **Not configured** | **Not configured** |
| Fail-fast | **Not configured** | **Not configured** |
**Gaps:** The backend has zero static analysis or architecture enforcement. Claude Code hooks don't exist yet. Neither side has fail-fast configured.
### 7. Evaluation: What to Implement
| Measure | Effort | Impact | Privacy OK | Maintained | Recommendation |
|---------|--------|--------|------------|------------|----------------|
| Claude Code Hooks (PostToolUse) | Low | High | Yes (local) | N/A (config) | **Immediate** |
| Fail-fast + output filtering | Low | Medium | Yes (local) | N/A (config) | **Immediate** |
| Checkstyle Maven plugin | Low | Medium | Yes (no network) | Yes (LGPL) | **Yes** |
| SpotBugs Maven plugin | Low | Medium | Yes (no network) | Yes (LGPL) | **Yes** |
| ArchUnit hexagonal tests | Medium | High | Yes (no network) | Yes (Apache) | **Yes** |
| Error Prone | Medium | Medium | Yes (no network) | Yes (Apache) | **Defer** — overlaps with SpotBugs, more invasive setup, Java 25 compatibility uncertain |
| Custom ESLint rules | Low | Low-Medium | Yes (local) | N/A (project rules) | **As needed** — add rules when recurring agent mistakes are observed |
| MCP LSP Server | High | Medium | Yes (local) | Varies | **Defer** — experimental, high setup cost, unclear benefit vs. hooks |
### 8. Tool Compatibility Notes
**Java 25 compatibility:**
- Checkstyle: Confirmed support for Java 21+, Java 25 should work (runs on source, not bytecode)
- SpotBugs: Bytecode analysis — needs ASM version that supports Java 25 classfiles. Latest SpotBugs (4.9.x) supports up to Java 24; Java 25 support may require a newer release. **Verify before adopting.**
- ArchUnit: Runs via JUnit, analyzes compiled classes. Similar ASM dependency concern as SpotBugs. **Verify before adopting.**
- Error Prone: Tightly coupled to javac internals. Java 25 compatibility typically lags. **Higher risk.**
**Privacy compliance:** All recommended tools are offline-only. None phone home, none require external services. All are open source with permissive or copyleft licenses compatible with GPL.
## Decisions Required
| # | Decision | Options | Recommendation |
|---|----------|---------|----------------|
| 1 | Hooks in which settings file? | `.claude/settings.json` (project, committed) vs. `.claude/settings.local.json` (personal, gitignored) | **Project-level** — every agent user benefits |
| 2 | Checkstyle ruleset | Google Style vs. Sun Style vs. custom | **Google Style** — most widely adopted, well-documented |
| 3 | Include Error Prone in plan? | Yes (more coverage) vs. defer (simpler, overlap with SpotBugs) | **Defer** — Java 25 compatibility uncertain, overlaps with SpotBugs |
## References
- Geoffrey Huntley: [Don't waste your back pressure](https://ghuntley.com/pressure/)
- JW: [If you don't engineer backpressure, you'll get slopped](https://jw.hn/engineering-backpressure)
- HumanLayer: [Context-Efficient Backpressure for Coding Agents](https://www.hlyr.dev/blog/context-efficient-backpressure)
- Anthropic: [Claude Code Hooks Reference](https://code.claude.com/docs/en/hooks)
- Anthropic: [2026 Agentic Coding Trends Report](https://resources.anthropic.com/hubfs/2026%20Agentic%20Coding%20Trends%20Report.pdf)
- ArchUnit: [User Guide](https://www.archunit.org/userguide/html/000_Index.html)
- ArchUnit Hexagonal: [GitHub](https://github.com/whiskeysierra/archunit-hexagonal)
- SpotBugs: [Documentation](https://spotbugs.github.io/)
- Checkstyle: [Documentation](https://checkstyle.sourceforge.io/)
- Claude Code Hooks Guide: [Luiz Tanure](https://www.letanure.dev/blog/2025-08-06--claude-code-part-8-hooks-automated-quality-checks)
- lsp-mcp: [GitHub](https://github.com/jonrad/lsp-mcp)

View File

@@ -0,0 +1,107 @@
---
date: 2026-03-04T21:15:50+00:00
git_commit: b8421274b47c6d1778b83c6b0acb70fd82891e71
branch: master
topic: "Date/Time Handling Best Practices for the fete Stack"
tags: [research, datetime, java, postgresql, openapi, typescript]
status: complete
---
# Research: Date/Time Handling Best Practices
## Research Question
What are the best practices for handling dates and times across the full fete stack (Java 25 / Spring Boot 3.5.x / PostgreSQL / OpenAPI 3.1 / Vue 3 / TypeScript)?
## Summary
The project has two distinct date/time concepts: **event date/time** (when something happens) and **expiry date** (after which data is deleted). These map to different types at every layer. The recommendations align Java types, PostgreSQL column types, OpenAPI formats, and TypeScript representations into a consistent stack-wide approach.
## Detailed Findings
### Type Mapping Across the Stack
| Concept | Java | PostgreSQL | OpenAPI | TypeScript | Example |
|---------|------|------------|---------|------------|---------|
| Event date/time | `OffsetDateTime` | `timestamptz` | `string`, `format: date-time` | `string` | `2026-03-15T20:00:00+01:00` |
| Expiry date | `LocalDate` | `date` | `string`, `format: date` | `string` | `2026-06-15` |
| Audit timestamps (createdAt, etc.) | `OffsetDateTime` | `timestamptz` | `string`, `format: date-time` | `string` | `2026-03-04T14:22:00Z` |
### Event Date/Time: `OffsetDateTime` + `timestamptz`
**Why `OffsetDateTime`, not `LocalDateTime`:**
- PostgreSQL best practice explicitly recommends `timestamptz` over `timestamp` — the PostgreSQL wiki says ["don't use `timestamp`"](https://wiki.postgresql.org/wiki/Don't_Do_This). `timestamptz` maps naturally to `OffsetDateTime`.
- Hibernate 6 (Spring Boot 3.5.x) has native `OffsetDateTime``timestamptz` support. `LocalDateTime` requires extra care to avoid silent timezone bugs at the JDBC driver level.
- An ISO 8601 string with offset (`2026-03-15T20:00:00+01:00`) is unambiguous in the API. A bare `LocalDateTime` string forces the client to guess the timezone.
- The OpenAPI `date-time` format and `openapi-generator` default to `OffsetDateTime` in Java — no custom type mappings needed.
**Why not `ZonedDateTime`:** Carries IANA zone IDs (e.g. `Europe/Berlin`) which add complexity without value for this use case. Worse JDBC support than `OffsetDateTime`.
**How PostgreSQL stores it:** `timestamptz` does **not** store the timezone. It converts input to UTC and stores UTC. On retrieval, it converts to the session's timezone setting. The offset is preserved in the Java `OffsetDateTime` via the JDBC driver.
**Practical flow:** The frontend sends the offset based on the organizer's browser locale. The server stores UTC. Display-side conversion happens in the frontend.
### Expiry Date: `LocalDate` + `date`
The expiry date is a calendar-date concept ("after which day should data be deleted"), not a point-in-time. A cleanup job runs periodically and deletes events where `expiryDate < today`. Sub-day precision adds no value and complicates the UX.
### Jackson Serialization (Spring Boot 3.5.x)
Spring Boot 3.x auto-configures `jackson-datatype-jsr310` (JavaTimeModule) and disables `WRITE_DATES_AS_TIMESTAMPS` by default:
- `OffsetDateTime` serializes to `"2026-03-15T20:00:00+01:00"` (ISO 8601 string)
- `LocalDate` serializes to `"2026-06-15"`
No additional configuration needed. For explicitness, can add to `application.properties`:
```properties
spring.jackson.serialization.write-dates-as-timestamps=false
```
### Hibernate 6 Configuration
With Hibernate 6, `OffsetDateTime` maps to `timestamptz` using the `NATIVE` timezone storage strategy by default on PostgreSQL. Can be made explicit:
```properties
spring.jpa.properties.hibernate.timezone.default_storage=NATIVE
```
This tells Hibernate to use the database's native `TIMESTAMP WITH TIME ZONE` type directly.
### OpenAPI Schema Definitions
```yaml
# Event date/time
eventDateTime:
type: string
format: date-time
example: "2026-03-15T20:00:00+01:00"
# Expiry date
expiryDate:
type: string
format: date
example: "2026-06-15"
```
**Code-generation mapping (defaults, no customization needed):**
| OpenAPI format | Java type (openapi-generator) | TypeScript type (openapi-typescript) |
|---------------|-------------------------------|--------------------------------------|
| `date-time` | `java.time.OffsetDateTime` | `string` |
| `date` | `java.time.LocalDate` | `string` |
### Frontend (TypeScript)
`openapi-typescript` generates `string` for both `format: date-time` and `format: date`. This is correct — JSON has no native date type, so dates travel as strings. Parsing to `Date` objects happens explicitly at the application boundary when needed (e.g. for display formatting).
## Sources
- [PostgreSQL Wiki: Don't Do This](https://wiki.postgresql.org/wiki/Don't_Do_This) — recommends `timestamptz` over `timestamp`
- [PostgreSQL Docs: Date/Time Types](https://www.postgresql.org/docs/current/datatype-datetime.html)
- [Thorben Janssen: Hibernate 6 OffsetDateTime and ZonedDateTime](https://thorben-janssen.com/hibernate-6-offsetdatetime-and-zoneddatetime/)
- [Baeldung: OffsetDateTime Serialization With Jackson](https://www.baeldung.com/java-jackson-offsetdatetime)
- [Baeldung: Map Date Types With OpenAPI Generator](https://www.baeldung.com/openapi-map-date-types)
- [Baeldung: ZonedDateTime vs OffsetDateTime](https://www.baeldung.com/java-zoneddatetime-offsetdatetime)
- [Reflectoring: Handling Timezones in Spring Boot](https://reflectoring.io/spring-timezones/)
- [openapi-typescript documentation](https://openapi-ts.dev/)

View File

@@ -0,0 +1,273 @@
---
date: 2026-03-05T10:14:52+00:00
git_commit: ffea279b54ad84be09bd0e82b3ed9c89a95fc606
branch: master
topic: "End-to-End Testing for Vue 3 with Playwright"
tags: [research, e2e, playwright, testing, frontend]
status: complete
---
# Research: End-to-End Testing for Vue 3 with Playwright
## Research Question
How to set up and structure end-to-end tests for the fete Vue 3 + Vite frontend using Playwright?
## Summary
Playwright is Vue 3's officially recommended E2E testing framework. It integrates with Vite projects through a `webServer` config block (no Vite plugin needed), supports Chromium/Firefox/WebKit under a single API, and is fully free including parallelism. The fete project's existing vitest.config.ts already excludes `e2e/**`, making the integration path clean.
## Detailed Findings
### 1. Current Frontend Test Infrastructure
The project uses **Vitest 4.0.18** with jsdom for unit/component tests:
- **Config:** `frontend/vitest.config.ts` — merges with vite.config, uses jsdom environment, bail on first failure
- **Exclusion:** Already excludes `e2e/**` from Vitest's test discovery (`vitest.config.ts:10`)
- **Existing tests:** 3 test files with ~25 tests total:
- `src/composables/__tests__/useEventStorage.spec.ts` (6 tests)
- `src/views/__tests__/EventCreateView.spec.ts` (11 tests)
- `src/views/__tests__/EventStubView.spec.ts` (8 tests)
- **No E2E framework** is currently configured
### 2. Why Playwright
Vue's official testing guide ([vuejs.org/guide/scaling-up/testing](https://vuejs.org/guide/scaling-up/testing)) positions Playwright as the primary E2E recommendation. Key advantages over Cypress:
| Dimension | Playwright | Cypress |
|---|---|---|
| Browser support | Chromium, Firefox, WebKit | Chrome-family, Firefox (WebKit experimental) |
| Parallelism | Free, native | Requires paid Cypress Cloud |
| Architecture | Out-of-process (CDP/BiDi) | In-browser (same process) |
| Speed | 35-45% faster in parallel | Slower at scale |
| Pricing | 100% free, Apache 2.0 | Cloud features cost money |
| Privacy | No account, no cloud dependency | Cloud service integration |
Playwright aligns with fete's privacy constraints (no cloud dependency, no account required).
### 3. Playwright + Vite Integration
Playwright does **not** use a Vite plugin. Integration is purely through process management:
1. Playwright reads `webServer.command` and spawns the Vite dev server
2. Polls `webServer.url` until ready
3. Runs tests against `use.baseURL`
4. Kills the server after all tests finish
The existing Vite dev proxy (`/api``localhost:8080`) works transparently — E2E tests can hit the real backend or intercept via `page.route()` mocks.
Note: `@playwright/experimental-ct-vue` exists for component-level testing (mounting individual Vue components without a server), but is still experimental and is a different category from E2E.
### 4. Installation
```bash
cd frontend
npm install --save-dev @playwright/test
npx playwright install --with-deps chromium
```
Using `npm init playwright@latest` generates scaffolding automatically, but for an existing project manual setup is cleaner.
### 5. Project Structure
```
frontend/
playwright.config.ts # Playwright config
e2e/ # E2E test directory
home.spec.ts
event-create.spec.ts
event-view.spec.ts
fixtures/ # shared test fixtures (optional)
helpers/ # page object models (optional)
playwright-report/ # generated HTML report (gitignored)
test-results/ # generated artifacts (gitignored)
```
The `e2e/` directory is already excluded from Vitest via `vitest.config.ts:10`.
### 6. Recommended playwright.config.ts
```typescript
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: process.env.CI ? 'github' : 'html',
use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
// Uncomment for cross-browser coverage:
// { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
// { name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
stdout: 'pipe',
},
})
```
Key decisions:
- `testDir: './e2e'` — separates E2E from Vitest unit tests
- `forbidOnly: !!process.env.CI` — prevents `test.only` from shipping to CI
- `workers: process.env.CI ? 1 : undefined` — single worker in CI avoids shared-state flakiness; locally uses all cores
- `reporter: 'github'` — GitHub Actions annotations in CI
- `command: 'npm run dev'` — runs `generate:api` first (via the existing npm script), then starts Vite
- `reuseExistingServer: !process.env.CI` — reuses running dev server locally for fast iteration
### 7. package.json Scripts
```json
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug"
```
### 8. .gitignore Additions
```
playwright-report/
test-results/
```
### 9. TypeScript Configuration
The existing `tsconfig.app.json` excludes `src/**/__tests__/*`. Since E2E tests live in `e2e/` (outside `src/`), they are already excluded from the app build.
A separate `tsconfig` for E2E tests is not strictly required — Playwright's own TypeScript support handles it. If needed, a minimal `e2e/tsconfig.json` can extend `tsconfig.node.json`.
### 10. Vue-Specific Testing Patterns
**Router navigation:**
```typescript
await page.goto('/events/abc-123')
await page.waitForURL('/events/abc-123') // confirms SPA router resolved
```
**Waiting for reactive content (auto-retry):**
```typescript
await expect(page.getByRole('heading', { name: 'My Event' })).toBeVisible()
// Playwright auto-retries assertions for up to the configured timeout
```
**URL assertions:**
```typescript
await expect(page).toHaveURL(/\/events\/.+/)
```
**API mocking (for isolated E2E tests):**
```typescript
await page.route('/api/events/**', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ title: 'Test Event', date: '2026-04-01' }),
})
})
```
**Locator strategy — prefer accessible locators:**
```typescript
page.getByRole('button', { name: 'RSVP' }) // best
page.getByLabel('Event Title') // form fields
page.getByTestId('event-card') // data-testid fallback
page.locator('.some-class') // last resort
```
### 11. CI Integration
**GitHub Actions workflow:**
```yaml
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
# --with-deps installs OS-level libraries (libglib, libnss, etc.)
# Specify 'chromium' to save ~2min vs installing all browsers
- name: Run E2E tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: frontend/playwright-report/
retention-days: 30
```
**Docker:** Use official images `mcr.microsoft.com/playwright:v1.x.x-noble` (Ubuntu 24.04). Alpine is unsupported (browsers need glibc). Key flag: `--ipc=host` prevents Chromium memory exhaustion. The Playwright Docker image version must match the `@playwright/test` package version exactly.
For the fete project, E2E tests run as a separate CI step, not inside the app's Dockerfile.
### 12. Integration with Existing Backend
Two approaches for E2E tests:
1. **Mocked backend** (via `page.route()`): Fast, isolated, no backend dependency. Good for frontend-only testing.
2. **Real backend**: Start Spring Boot alongside Vite. Tests hit `/api` through the Vite proxy. More realistic but requires Java in CI. Could use Docker Compose.
The Vite proxy config (`vite.config.ts:19-23`) already forwards `/api` to `localhost:8080`, so both approaches work without changes.
## Code References
- `frontend/vitest.config.ts:10` — E2E exclusion pattern already in place
- `frontend/vite.config.ts:19-23` — API proxy configuration for backend integration
- `frontend/package.json:8-9``dev` script runs `generate:api` before Vite
- `frontend/src/router/index.ts` — Route definitions (Home, Create, Event views)
- `frontend/src/api/client.ts` — openapi-fetch client using `/api` base URL
- `frontend/tsconfig.app.json` — App TypeScript config (excludes test files)
## Architecture Documentation
### Test Pyramid in fete
| Layer | Framework | Directory | Purpose |
|---|---|---|---|
| Unit | Vitest + jsdom | `src/**/__tests__/` | Composables, isolated logic |
| Component | Vitest + @vue/test-utils | `src/**/__tests__/` | Vue component behavior |
| E2E | Playwright (proposed) | `e2e/` | Full browser, user flows |
| Visual | browser-interactive-testing skill | `.agent-tests/` | Agent-driven screenshots |
### Decision Points for Implementation
1. **Start with Chromium only** — add Firefox/WebKit later if needed
2. **Use `npm run dev`** as webServer command (includes API type generation)
3. **API mocking by default** — use `page.route()` for E2E isolation; full-stack tests as a separate concern
4. **`data-testid` attributes** on key interactive elements for stable selectors
5. **Page Object Model** recommended once the test suite grows beyond 5-10 tests
## Sources
- [Testing | Vue.js](https://vuejs.org/guide/scaling-up/testing) — official E2E recommendation
- [Installation | Playwright](https://playwright.dev/docs/intro)
- [webServer | Playwright](https://playwright.dev/docs/test-webserver) — Vite integration
- [CI Intro | Playwright](https://playwright.dev/docs/ci-intro)
- [Docker | Playwright](https://playwright.dev/docs/docker)
- [Cypress vs Playwright 2026 | BugBug](https://bugbug.io/blog/test-automation-tools/cypress-vs-playwright/)
- [Playwright vs Cypress | Katalon](https://katalon.com/resources-center/blog/playwright-vs-cypress)
## Decisions (2026-03-05)
- **Mocked backend only** — E2E tests use `page.route()` to mock API responses. No real Spring Boot backend in E2E.
- **Mocking stack:** `@msw/playwright` + `@msw/source` — reads OpenAPI spec at runtime, generates MSW handlers, per-test overrides via `network.use()`.
- **US-1 flows first** — Event creation is the only implemented user story; E2E tests cover that flow.
- **No CI caching yet** — Playwright browser binaries are not cached; CI runner needs reconfiguration first.
- **E2E tests are part of frontend tasks** — every frontend user story includes E2E test coverage going forward.
- **OpenAPI examples mandatory** — all response schemas in the OpenAPI spec must include `example:` fields (required for `@msw/source` mock generation).

View File

@@ -0,0 +1,215 @@
---
date: "2026-03-04T22:27:37.933286+00:00"
git_commit: 91e566efea0cbf53ba06a29b63317b7435609bd8
branch: master
topic: "Automatic OpenAPI Validation Pipelines for Backpressure Hooks"
tags: [research, openapi, validation, hooks, backpressure, linting]
status: complete
---
# Research: Automatic OpenAPI Validation Pipelines
## Research Question
What automatic validation pipelines exist for OpenAPI specs that can be integrated into the current Claude Code backpressure hook setup, running after the OpenAPI spec has been modified?
## Summary
The project already has a PostToolUse hook system that runs backend compile checks and frontend lint/type-checks after Edit/Write operations. Adding OpenAPI spec validation requires a new hook script that triggers specifically when `api.yaml` is modified. Several CLI tools support OpenAPI 3.1.0 validation — **Redocly CLI** is the strongest fit given the existing Node.js toolchain, MIT license, active maintenance, and zero-config baseline.
## Current Backpressure Setup
### Hook Architecture (`.claude/settings.json`)
The project uses Claude Code hooks for automated quality gates:
| Hook Event | Trigger | Scripts |
|---|---|---|
| `PostToolUse` | `Edit\|Write` tool calls | `backend-compile-check.sh`, `frontend-check.sh` |
| `Stop` | Agent attempts to stop | `run-tests.sh` |
### How Hooks Work
Each hook script:
1. Reads JSON from stdin containing `tool_input.file_path`
2. Pattern-matches the file path to decide if it should run
3. Executes validation (compile, lint, type-check, test)
4. Returns JSON with either success message or failure details
5. On failure: outputs `hookSpecificOutput` with error context (PostToolUse) or `{"decision":"block"}` (Stop)
### Existing Pattern for File Matching
```bash
# backend-compile-check.sh — matches Java files
case "$FILE_PATH" in
*/backend/src/*.java|backend/src/*.java) ;;
*) exit 0 ;;
esac
# frontend-check.sh — matches TS/Vue files
case "$FILE_PATH" in
*/frontend/src/*.ts|*/frontend/src/*.vue|frontend/src/*.ts|frontend/src/*.vue) ;;
*) exit 0 ;;
esac
```
An OpenAPI validation hook would use the same pattern:
```bash
case "$FILE_PATH" in
*/openapi/api.yaml|*/openapi/*.yaml) ;;
*) exit 0 ;;
esac
```
### Existing OpenAPI Tooling in the Project
- **Backend:** `openapi-generator-maven-plugin` v7.20.0 generates Spring interfaces from `api.yaml` (`pom.xml:149-178`)
- **Frontend:** `openapi-typescript` v7.13.0 generates TypeScript types; `openapi-fetch` v0.17.0 provides type-safe client
- **No validation/linting tools** currently installed — no Redocly, Spectral, or other linter config exists
## Tool Evaluation
### Redocly CLI (`@redocly/cli`)
| Attribute | Value |
|---|---|
| OpenAPI 3.1 | Full support |
| Install | `npm install -g @redocly/cli` or `npx @redocly/cli@latest` |
| CLI | `redocly lint api.yaml` |
| License | MIT |
| Maintenance | Very active — latest v2.20.3 (2026-03-03), daily/weekly releases |
| GitHub | ~1.4k stars (Redocly ecosystem: 24k+ combined) |
**Checks:** Structural validity against OAS schema, configurable linting rules (naming, descriptions, operation IDs, security), style/consistency enforcement. Built-in rulesets: `minimal`, `recommended`, `recommended-strict`. Zero-config baseline works immediately. Custom rules via `redocly.yaml`.
**Fit for this project:** Node.js already in the toolchain (frontend). `npx` form requires no permanent install. MIT license compatible with GPL-3.0. The `@redocly/openapi-core` package is already present as a transitive dependency of `openapi-typescript` in `node_modules`.
### Spectral (`@stoplight/spectral-cli`)
| Attribute | Value |
|---|---|
| OpenAPI 3.1 | Full support (since v6.x) |
| Install | `npm install -g @stoplight/spectral-cli` |
| CLI | `spectral lint api.yaml` |
| License | Apache 2.0 |
| Maintenance | Active — latest v6.15.0 (2025-04-22), slower cadence |
| GitHub | ~3k stars |
**Checks:** Schema compliance, missing descriptions/tags/operationIds, contact/license metadata. Highly extensible custom rulesets via YAML/JS. Configurable severity levels.
**Fit for this project:** Well-established industry standard. Apache 2.0 compatible with GPL. Less actively maintained than Redocly (10 months since last release). Heavier custom ruleset system may be over-engineered for current needs.
### Vacuum (`daveshanley/vacuum`)
| Attribute | Value |
|---|---|
| OpenAPI 3.1 | Full support (via libopenapi) |
| Install | `brew install daveshanley/vacuum/vacuum` or Go binary |
| CLI | `vacuum lint api.yaml` |
| License | MIT |
| Maintenance | Active — latest release 2025-12-22 |
| GitHub | ~1k stars |
**Checks:** Structural validation, Spectral-compatible rulesets, OWASP security checks, naming conventions, descriptions/examples/tags. Single Go binary — no runtime dependencies.
**Fit for this project:** Zero-dependency binary is appealing for CI. However, adds a non-Node.js tool dependency when the project already has Node.js. Spectral ruleset compatibility is a plus for portability.
### oasdiff (`oasdiff/oasdiff`)
| Attribute | Value |
|---|---|
| OpenAPI 3.1 | Beta |
| Install | `brew install oasdiff` or Go binary |
| CLI | `oasdiff breaking base.yaml revision.yaml` |
| License | Apache 2.0 |
| Maintenance | Active — latest v1.11.10 (2026-02-05) |
| GitHub | ~1.1k stars |
**Checks:** 300+ breaking change detection rules (paths, parameters, schemas, security, headers, enums). Requires two spec versions to compare — not a standalone validator.
**Fit for this project:** Different category — detects breaking changes between spec versions, not structural validity. Useful as a CI-only check comparing `HEAD~1` vs `HEAD`. OAS 3.1 support is still beta.
### Not Recommended
- **swagger-cli:** Abandoned, no OAS 3.1 support
- **IBM OpenAPI Validator:** Active but opinionated IBM-specific rules add configuration overhead for no benefit
## Tool Comparison Matrix
| Tool | OAS 3.1 | License | Last Release | Stars | Runtime | Category |
|---|---|---|---|---|---|---|
| **Redocly CLI** | Full | MIT | 2026-03-03 | ~1.4k | Node.js | Lint + validate |
| **Spectral** | Full | Apache 2.0 | 2025-04-22 | ~3k | Node.js | Lint |
| **Vacuum** | Full | MIT | 2025-12-22 | ~1k | Go binary | Lint + validate |
| **oasdiff** | Beta | Apache 2.0 | 2026-02-05 | ~1.1k | Go binary | Breaking changes |
## Integration Pattern
### Hook Script Structure
An OpenAPI validation hook would follow the existing pattern in `.claude/hooks/`:
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('file_path',''))" 2>/dev/null || echo "")
# Only run for OpenAPI spec files
case "$FILE_PATH" in
*/openapi/*.yaml|*/openapi/*.yml) ;;
*) exit 0 ;;
esac
cd "$CLAUDE_PROJECT_DIR/backend"
# Run validation
if OUTPUT=$(npx @redocly/cli@latest lint src/main/resources/openapi/api.yaml --format=stylish 2>&1); then
echo '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"✓ OpenAPI spec validation passed."}}'
else
ESCAPED=$(echo "$OUTPUT" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":$ESCAPED}}"
fi
```
### Registration in `.claude/settings.json`
The hook would be added to the existing `PostToolUse` array alongside the compile and lint hooks:
```json
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/openapi-validate.sh\"",
"timeout": 120
}
```
### Configuration (Optional)
A `redocly.yaml` in the project root or `backend/` directory can customize rules:
```yaml
extends:
- recommended
rules:
operation-operationId: error
tag-description: warn
no-ambiguous-paths: error
```
## Code References
- `.claude/settings.json:1-32` — Hook configuration (PostToolUse + Stop events)
- `.claude/hooks/backend-compile-check.sh` — Java file detection pattern + compile check
- `.claude/hooks/frontend-check.sh` — TS/Vue file detection pattern + type-check + lint
- `.claude/hooks/run-tests.sh` — Stop hook with test execution and block/approve logic
- `backend/pom.xml:149-178` — openapi-generator-maven-plugin configuration
- `backend/src/main/resources/openapi/api.yaml` — The OpenAPI 3.1.0 spec to validate
## Open Questions
- Should the validation use a pinned version (`npx @redocly/cli@1.x.x`) or latest? Pinned is more reproducible; latest gets rule updates automatically.
- Should a `redocly.yaml` config be added immediately with the `recommended` ruleset, or start with zero-config (structural validation only) and add rules incrementally?
- Is breaking change detection (oasdiff) desirable as a separate CI check, or is structural validation sufficient for now?

View File

@@ -0,0 +1,202 @@
---
date: 2026-03-04T21:15:50+00:00
git_commit: b8421274b47c6d1778b83c6b0acb70fd82891e71
branch: master
topic: "RFC 9457 Problem Details for HTTP API Error Responses"
tags: [research, error-handling, rfc9457, spring-boot, openapi]
status: complete
---
# Research: RFC 9457 Problem Details
## Research Question
How should the fete API structure error responses? What does RFC 9457 (Problem Details) specify, and how does it integrate with Spring Boot 3.5.x, OpenAPI 3.1, and openapi-fetch?
## Summary
RFC 9457 (successor to RFC 7807) defines a standard JSON format (`application/problem+json`) for machine-readable HTTP API errors. Spring Boot 3.x has first-class support via `ProblemDetail`, `ErrorResponseException`, and `ResponseEntityExceptionHandler`. The recommended approach is a single `@RestControllerAdvice` that handles all exceptions consistently — no `spring.mvc.problemdetails.enabled` property, no fallback to legacy error format.
## Detailed Findings
### RFC 9457 Format
Standard fields:
| Field | Type | Description |
|-------|------|-------------|
| `type` | URI | Identifies the problem type. Defaults to `about:blank`. |
| `title` | string | Short, human-readable summary. Should not change between occurrences. |
| `status` | int | HTTP status code. |
| `detail` | string | Human-readable explanation specific to this occurrence. |
| `instance` | URI | Identifies the specific occurrence (e.g. correlation ID). |
Extension members (additional JSON properties) are explicitly permitted. This is the mechanism for validation errors, error codes, etc.
**Key rule:** With `type: "about:blank"`, the `title` must match the HTTP status phrase exactly. Use a custom `type` URI when providing a custom `title`.
### Spring Boot 3.x Built-in Support
- **`ProblemDetail`** — container class for the five standard fields + a `properties` Map for extensions.
- **`ErrorResponseException`** — base class for custom exceptions that carry their own `ProblemDetail`.
- **`ResponseEntityExceptionHandler`** — `@ControllerAdvice` base class that handles all Spring MVC exceptions and renders them as `application/problem+json`.
- **`ProblemDetailJacksonMixin`** — automatically unwraps the `properties` Map as top-level JSON fields during serialization.
### Recommended Configuration
Use a single `@RestControllerAdvice` extending `ResponseEntityExceptionHandler`. Do **not** use the `spring.mvc.problemdetails.enabled` property.
```java
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
// All Spring MVC exceptions are handled automatically.
// Add @ExceptionHandler methods for domain exceptions here.
// Add a catch-all for Exception.class to prevent legacy error format.
}
```
Reasons to avoid the property-based approach:
1. No place to add custom `@ExceptionHandler` methods.
2. Having both the property AND a custom `ResponseEntityExceptionHandler` bean causes a conflict.
3. The property ignores `server.error.include-*` properties.
### Validation Errors (Field-Level)
Spring deliberately does **not** include field-level validation errors in `ProblemDetail` by default (security rationale). Override `handleMethodArgumentNotValid`:
```java
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {
ProblemDetail problemDetail = ex.getBody();
problemDetail.setTitle("Validation Failed");
problemDetail.setType(URI.create("urn:problem-type:validation-error"));
List<Map<String, String>> fieldErrors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(fe -> Map.of(
"field", fe.getField(),
"message", fe.getDefaultMessage()
))
.toList();
problemDetail.setProperty("fieldErrors", fieldErrors);
return handleExceptionInternal(ex, problemDetail, headers, status, request);
}
```
Resulting response:
```json
{
"type": "urn:problem-type:validation-error",
"title": "Validation Failed",
"status": 400,
"detail": "Invalid request content.",
"instance": "/api/events",
"fieldErrors": [
{ "field": "title", "message": "must not be blank" },
{ "field": "expiryDate", "message": "must be a future date" }
]
}
```
### OpenAPI Schema Definition
```yaml
components:
schemas:
ProblemDetail:
type: object
properties:
type:
type: string
format: uri
default: "about:blank"
title:
type: string
status:
type: integer
detail:
type: string
instance:
type: string
format: uri
additionalProperties: true
ValidationProblemDetail:
allOf:
- $ref: '#/components/schemas/ProblemDetail'
- type: object
properties:
fieldErrors:
type: array
items:
type: object
properties:
field:
type: string
message:
type: string
required:
- field
- message
responses:
BadRequest:
description: Validation failed
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ValidationProblemDetail'
NotFound:
description: Resource not found
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetail'
```
Use media type `application/problem+json` in response definitions. Set `additionalProperties: true` on the base schema.
### Frontend Consumption (openapi-fetch)
openapi-fetch uses a discriminated union for responses:
```typescript
const { data, error } = await client.POST('/api/events', { body: eventData })
if (error) {
// `error` is typed from the OpenAPI error response schema
console.log(error.title) // "Validation Failed"
console.log(error.fieldErrors) // [{ field: "title", message: "..." }]
return
}
// `data` is the typed success response
```
The `error` object is already typed from the generated schema — no manual type assertions needed for defined error shapes.
### Known Pitfalls
| Pitfall | Description | Mitigation |
|---------|-------------|------------|
| **Inconsistent formats** | Exceptions escaping to Spring Boot's `BasicErrorController` return legacy format (`timestamp`, `error`, `path`), not Problem Details. | Add a catch-all `@ExceptionHandler(Exception.class)` in the `@RestControllerAdvice`. |
| **`server.error.include-*` ignored** | When Problem Details is active, these properties have no effect. | Control content via `ProblemDetail` directly. |
| **Validation errors hidden by default** | Spring returns only `"Invalid request content."` without field details. | Override `handleMethodArgumentNotValid` explicitly. |
| **Content negotiation** | `application/problem+json` is only returned when the client accepts it. `openapi-fetch` sends `Accept: application/json` which Spring considers compatible. | No action needed for SPA clients. |
| **`about:blank` semantics** | With `type: "about:blank"`, `title` must match the HTTP status phrase. Custom titles require a custom `type` URI. | Use `urn:problem-type:*` URIs for custom problem types. |
## Sources
- [RFC 9457 Full Text](https://www.rfc-editor.org/rfc/rfc9457.html)
- [Spring Framework Docs: Error Responses](https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-ann-rest-exceptions.html)
- [Swagger Blog: Problem Details RFC 9457](https://swagger.io/blog/problem-details-rfc9457-doing-api-errors-well/)
- [Baeldung: Returning Errors Using ProblemDetail](https://www.baeldung.com/spring-boot-return-errors-problemdetail)
- [SivaLabs: Spring Boot 3 Error Reporting](https://www.sivalabs.in/blog/spring-boot-3-error-reporting-using-problem-details/)
- [Spring Boot Issue #43850: Render global errors as Problem Details](https://github.com/spring-projects/spring-boot/issues/43850)

View File

@@ -0,0 +1,404 @@
# Research: Modern Sans-Serif Fonts for Mobile-First PWA
**Date:** 2026-03-04
**Context:** Selecting a primary typeface for fete, a privacy-focused PWA for event announcements and RSVPs. The font must be open-source with permissive licensing, modern geometric/neo-grotesque style, excellent mobile readability, and strong weight range.
---
## Executive Summary
Based on research of 9 candidate fonts, **6 meet all requirements** for self-hosting and redistribution under permissive licenses. Two do not qualify:
- **General Sans**: Proprietary (ITF Free Font License, non-commercial personal use only)
- **Satoshi**: License ambiguity; sources conflict between full OFL and ITF restrictions
The remaining **6 fonts are fully open-source** and suitable for the project:
| Font | License | Design | Weights | Status |
|------|---------|--------|---------|--------|
| Inter | OFL-1.1 | Neo-grotesque, humanist | 9 (ThinBlack) | ✅ Recommended |
| Plus Jakarta Sans | OFL-1.1 | Geometric, modern | 7 (ExtraLightExtraBold) | ✅ Recommended |
| Outfit | OFL-1.1 | Geometric | 9 (ThinBlack) | ✅ Recommended |
| Space Grotesk | OFL-1.1 | Neo-grotesque, distinctive | 5 (LightBold) | ✅ Recommended |
| Manrope | OFL-1.1 | Geometric, humanist | 7 (ExtraLightExtraBold) | ✅ Recommended |
| DM Sans | OFL-1.1 | Geometric, low-contrast | 9 (ThinBlack) | ✅ Recommended |
| Sora | OFL-1.1 | Geometric | 8 (ThinExtraBold) | ✅ Recommended |
---
## Detailed Candidate Analysis
### 1. Inter
**License:** SIL Open Font License 1.1 (OFL-1.1)
**Download Location:**
- **Official:** https://github.com/rsms/inter (releases page)
- **NPM:** `inter-ui` package
- **Homebrew:** `font-inter`
- **Official CDN:** https://rsms.me/inter/inter.css
**Design Character:** Neo-grotesque with humanist touches. High x-height for enhanced legibility on screens. Geometric letterforms with open apertures. Designed specifically for UI and on-screen use.
**Available Weights:** 9 weights from Thin (100) to Black (900), each with italic variant. Also available as a variable font with weight axis.
**Notable Apps/Products:**
- **UX/Design tools:** Figma, Notion, Pixar Presto
- **OS:** Elementary OS, GNOME
- **Web:** GitLab, ISO, Mozilla, NASA
- **Why:** Chosen by product teams valuing clarity and modern minimalism; default choice for UI designers
**Mobile Suitability:** Excellent. Specifically engineered for screen readability with high x-height and open apertures. Performs well at 1416px body text.
**Distinctive Strengths:**
- Purpose-built for digital interfaces
- Exceptional clarity in dense UI layouts
- Strong brand identity (recognizable across tech products)
- Extensive OpenType features
**Weakness:** Very widely used; less distinctive for a bold brand identity. Considered the "safe" choice.
---
### 2. Plus Jakarta Sans
**License:** SIL Open Font License 1.1 (OFL-1.1)
**Download Location:**
- **Official Repository:** https://github.com/tokotype/PlusJakartaSans
- **Source Files:** `sources/`, compiled fonts in `fonts/` directory
- **Designer Contact:** mail@tokotype.com (Gumpita Rahayu, Tokotype)
- **Latest Version:** 2.7.1 (May 2023)
- **Build Command:** `gftools builder sources/builder.yaml`
**Design Character:** Geometric sans-serif with modern, clean-cut forms. Inspired by Neuzeit Grotesk and Futura but with contemporary refinement. Slightly taller x-height for clear spacing between caps and lowercase. Open counters and balanced spacing for legibility across sizes. **Bold, distinctive look** with personality.
**Available Weights:** 7 weights from ExtraLight (200) to ExtraBold (800), with matching italics.
**Notable Apps/Products:**
- Original commission: Jakarta Provincial Government's "+Jakarta City of Collaboration" program (2020)
- Now widely used in: Branding projects, modern web design, UI design
- **Why:** Chosen for fresh, contemporary feel without generic blandness
**Mobile Suitability:** Excellent. Designed with mobile UI in mind. Clean letterforms render crisply on small screens.
**Distinctive Strengths:**
- **Stylistic sets:** Sharp, Straight, and Swirl variants add design flexibility
- Modern geometric with Indonesian design heritage (unique perspective)
- Excellent for branding (not generic like Inter)
- OpenType features for sophisticated typography
- Well-maintained, active development
**Weakness:** Less ubiquitous than Inter; smaller ecosystem of design tool integrations.
---
### 3. Outfit
**License:** SIL Open Font License 1.1 (OFL-1.1)
**Download Location:**
- **Official Repository:** https://github.com/Outfitio/Outfit-Fonts
- **Fonts Directory:** `/fonts` in repository
- **OFL Text:** `OFL.txt` in repository
- **Designer:** Rodrigo Fuenzalida (originally for Outfit.io)
- **Status:** Repository archived Feb 25, 2025 (read-only, downloads remain accessible)
**Design Character:** Geometric sans-serif with warm, friendly appearance. Generous x-height, balanced spacing, low contrast. Nine static weights plus variable font with weight axis.
**Available Weights:** 9 weights from Thin (100) to Black (900). No italics.
**Notable Apps/Products:**
- Originally created for Outfit.io platform
- Good readability for body text (≈16px) and strong headline presence
- Used in design tools (Figma integration)
**Mobile Suitability:** Good. Geometric forms and generous spacing work well on mobile, though low contrast may require careful pairing with sufficient color contrast.
**Distinctive Strengths:**
- Full weight range (ThinBlack)
- Variable font option for granular weight control
- Stylistic alternates and rare ligatures
- Accessible character set
**Weakness:** Archived repository; no active development. Low contrast design requires careful color/contrast pairing for accessibility.
---
### 4. Space Grotesk
**License:** SIL Open Font License 1.1 (OFL-1.1)
**Download Location:**
- **Official Repository:** https://github.com/floriankarsten/space-grotesk
- **Official Site:** https://fonts.floriankarsten.com/space-grotesk
- **Designer:** Florian Karsten
- **Variants:** Variable font with weight axis
**Design Character:** Neo-grotesque with distinctive personality. Proportional variant of Space Mono (Colophon Foundry, 2016). Retains Space Mono's idiosyncratic details while optimizing for improved readability. Bold, tech-forward aesthetic with monowidth heritage visible in character design.
**Available Weights:** 5 weights—Light (300), Regular (400), Medium (500), SemiBold (600), Bold (700). No italics.
**Notable Apps/Products:**
- Modern tech companies and startups seeking distinctive branding
- Popular in neo-brutalist web design
- Good for headlines and display use
**Mobile Suitability:** Good. Clean proportional forms with distinctive character. Works well for headlines; body text at 14px+ is readable.
**Distinctive Strengths:**
- **Bold, tech-forward personality** — immediately recognizable
- Heritage from Space Mono adds character without looking dated
- Excellent OpenType support (old-style figures, tabular figures, superscript, subscript, fractions, stylistic alternates)
- **Supports extended language coverage:** Latin, Vietnamese, Pinyin, Central/South-Eastern European
**Weakness:** Only 5 weights (lightest is 300, no Thin). Fewer weight options than Inter or DM Sans.
---
### 5. Manrope
**License:** SIL Open Font License 1.1 (OFL-1.1)
**Download Location:**
- **Official Repository:** https://github.com/sharanda/manrope
- **Designer:** Mikhail Sharanda (2018), converted to variable by Mirko Velimirovic (2019)
- **Alternative Sources:** Multiple community forks on GitHub, npm packages
- **NPM Package:** `@fontsource/manrope`, `@fontsource-variable/manrope`
**Design Character:** Modern geometric sans-serif blending geometric shapes with humanistic elements. Semi-condensed structure with clean, contemporary feel. Geometric digits, packed with OpenType features.
**Available Weights:** 7 weights from ExtraLight (200) to ExtraBold (800). Available as variable font.
**Notable Apps/Products:**
- Widely used in modern design systems
- Popular in product/SaaS design
- Good for both UI and branding
**Mobile Suitability:** Excellent. Clean geometric design with humanistic touches; balanced proportions work well on mobile.
**Distinctive Strengths:**
- Geometric + humanistic blend (best of both worlds)
- Well-maintained active project
- Variable font available
- Strong design community around the font
**Weakness:** None significant; solid all-around choice.
---
### 6. DM Sans
**License:** SIL Open Font License 1.1 (OFL-1.1)
**Download Location:**
- **Official Repository:** https://github.com/googlefonts/dm-fonts
- **Releases Page:** https://github.com/googlefonts/dm-fonts/releases
- **Google Fonts:** https://fonts.google.com/specimen/DM+Sans
- **Design:** Commissioned from Colophon Foundry; Creative Direction: MultiAdaptor & DeepMind
**Design Character:** Low-contrast geometric sans-serif optimized for text at smaller sizes. Part of the DM suite (DM Sans, DM Serif Text, DM Serif Display). Designed for clarity and efficiency in dense typography.
**Available Weights:** 9 weights from Thin (100) to Black (900), each with italic variant.
**Notable Apps/Products:**
- DeepMind products (by commission)
- Tech companies favoring geometric clarity
- Professional and commercial products requiring text legibility
**Mobile Suitability:** Excellent. Specifically optimized for small text sizes; low contrast minimizes visual noise on mobile screens.
**Distinctive Strengths:**
- **Optimized for small text** — superior at 1214px
- Full weight range (ThinBlack)
- Active Google Fonts maintenance
- Italic variants (unlike Outfit or Space Grotesk)
- Commissioned by reputable team (DeepMind)
**Weakness:** Low contrast may feel less bold on headlines without careful sizing/weight adjustment.
---
### 7. Sora
**License:** SIL Open Font License 1.1 (OFL-1.1)
**Download Location:**
- **Official Repository:** https://github.com/sora-xor/sora-font
- **GitHub Releases:** Direct TTF/OTF downloads available
- **NPM Packages:** `@fontsource/sora`, `@fontsource-variable/sora`
- **Original Purpose:** Custom typeface for SORA decentralized autonomous economy
**Design Character:** Geometric sans-serif with contemporary, clean aesthetic. Available as both static fonts and variable font. Designed as a branding solution for decentralized systems.
**Available Weights:** 8 weights from Thin (100) to ExtraBold (800), each with italic variant. Variable font available.
**Notable Apps/Products:**
- Sora (XOR) decentralized projects
- Crypto/blockchain projects using modern typography
- Web3 products seeking distinctive branding
**Mobile Suitability:** Good. Clean geometric forms render well on mobile; italics available for emphasis.
**Distinctive Strengths:**
- Full weight range with italics
- Variable font option
- Designed for digital-first branding
- GitHub-native distribution
**Weakness:** Less established than Inter or DM Sans in mainstream product design; smaller ecosystem.
---
## Rejected Candidates
### General Sans
**Status:** ❌ Does not meet licensing requirements
**License:** ITF Free Font License (proprietary, non-commercial personal use only)
**Why Rejected:** This is a **paid commercial font** distributed by the Indian Type Foundry (not open-source). The ITF Free Font License permits personal use only; commercial use requires a separate paid license. Does not meet the "open-source with permissive license" requirement.
**Designer:** Frode Helland (published by Indian Type Foundry)
---
### Satoshi
**Status:** ⚠️ License ambiguity — conflicting sources
**Documented License:**
- Some sources claim SIL Open Font License (OFL-1.1)
- Other sources indicate ITF Free Font License (personal use only) similar to General Sans
**Design:** Swiss-style modernist sans-serif (Light to Black, 510 weights)
**Download:** Fontshare (Indian Type Foundry's free font service)
**Why Not Recommended:** The license status is unclear. While Fontshare advertises "free for personal and commercial use," the font's origin (Indian Type Foundry) and conflicting license documentation create uncertainty. For a privacy-focused project with clear open-source requirements, Satoshi's ambiguous licensing creates unnecessary legal risk. Better alternatives with unambiguous OFL-1.1 licensing are available.
**Recommendation:** If clarity is needed, contact Fontshare/ITF directly. For now, exclude from consideration to reduce licensing complexity.
---
## Comparative Table: Qualified Fonts
| Metric | Inter | Plus Jakarta Sans | Outfit | Space Grotesk | Manrope | DM Sans | Sora |
|--------|-------|-------------------|--------|---------------|---------|---------|------|
| **License** | OFL-1.1 | OFL-1.1 | OFL-1.1 | OFL-1.1 | OFL-1.1 | OFL-1.1 | OFL-1.1 |
| **Weights** | 9 | 7 | 9 | 5 | 7 | 9 | 8 |
| **Italics** | ✅ Yes | ✅ Yes | ❌ No | ❌ No | ❌ No | ✅ Yes | ✅ Yes |
| **Variable Font** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| **Design** | Neo-grotesque | Geometric | Geometric | Neo-grotesque | Geo + Humanist | Geometric | Geometric |
| **Personality** | Generic/Safe | Bold/Fresh | Warm/Friendly | Tech-Forward | Balanced | Efficient/Clean | Contemporary |
| **Mobile Text** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| **Distinctiveness** | Low | High | Medium | High | High | Medium | Medium |
| **Ecosystem** | Very Large | Growing | Medium | Growing | Growing | Large | Small |
| **Active Dev** | ✅ Yes | ✅ Yes | ❌ Archived | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
---
## Recommendations
### For Bold App-Native Branding
**Primary Choice: Plus Jakarta Sans**
**Rationale:**
- Fully open-source (OFL-1.1) with unambiguous licensing
- Bold, modern geometric aesthetic suitable for app branding
- Stylistic sets (Sharp, Straight, Swirl) provide design flexibility
- Well-maintained by Tokotype with clear development history
- Strong presence in modern UI/web design
- Excellent mobile readability with thoughtful character spacing
- Indonesian design heritage adds unique perspective (not generic)
**Alternative: Space Grotesk**
If you prefer **even more distinctive character:**
- Neo-grotesque with tech-forward personality
- Smaller weight range (5 weights) but strong identity
- Popular in contemporary design circles
- Good for headlines; pair with a more neutral font for body text if needed
---
### For Safe, Professional UI
**Primary Choice: Inter or DM Sans**
**Inter if:**
- Maximum ecosystem and tool support desired
- Designing for broad recognition and trust
- Team already familiar with Inter (widespread in tech)
**DM Sans if:**
- Emphasis on small text legibility (optimized for 1214px)
- Prefer italic variants
- Want active maintenance from Google Fonts community
---
### For Balanced Approach
**Manrope**
- Geometric + humanistic blend (versatile)
- Excellent mobile performance
- Strong weight range (7 weights)
- Underrated choice; often overlooked for bolder options but delivers polish
---
## Implementation Notes for Self-Hosting
All recommended fonts can be self-hosted:
1. **Download:** Clone repository or download from releases page
2. **Generate Web Formats:** Use FontForge, FontTools, or online converters to generate WOFF2 (required for modern browsers)
3. **CSS:** Include via `@font-face` with local file paths
4. **License:** Include `LICENSE.txt` or `OFL.txt` in the distribution
Example self-hosted CSS:
```css
@font-face {
font-family: 'Plus Jakarta Sans';
src: url('/fonts/PlusJakartaSans-Regular.woff2') format('woff2');
font-weight: 400;
font-display: swap;
}
```
---
## Privacy Considerations
All selected fonts are self-hosted open-source projects with no telemetry, no external CDN dependencies, and no tracking. Fully compliant with the project's privacy-first principles.
---
## Conclusion
**Inter, Plus Jakarta Sans, and Space Grotesk** are the strongest candidates. The choice depends on brand positioning:
- **Generic + Safe → Inter**
- **Bold + Modern → Plus Jakarta Sans**
- **Tech-Forward + Distinctive → Space Grotesk**
All seven recommended fonts meet the strict licensing, openness, mobile readability, and weight-range requirements. Any of them are viable; the decision is primarily aesthetic.
---
## Sources
- [Inter Font GitHub Repository](https://github.com/rsms/inter)
- [Plus Jakarta Sans GitHub Repository](https://github.com/tokotype/PlusJakartaSans)
- [Outfit Fonts GitHub Repository](https://github.com/Outfitio/Outfit-Fonts)
- [Space Grotesk GitHub Repository](https://github.com/floriankarsten/space-grotesk)
- [Manrope GitHub Repository](https://github.com/sharanda/manrope)
- [DM Fonts GitHub Repository](https://github.com/googlefonts/dm-fonts)
- [Sora Font GitHub Repository](https://github.com/sora-xor/sora-font)
- [SIL Open Font License](https://openfontlicense.org/)
- [Google Fonts (reference)](https://fonts.google.com)
- [Fontshare (reference)](https://www.fontshare.com)