API Server
February 12, 2026 · View on GitHub
The API server provides a RESTful interface for querying eIDAS Wallet Relying Party (WRP) registry data. It supports advanced filtering, full-text search, cursor-based pagination, and dynamic filter discovery.
Architecture
The server follows a 4-layer clean architecture with strict dependency rules (no upward imports):
┌──────────────────────────────────────┐
│ HTTP Handlers (internal/server/) │ ← Request parsing, response formatting
├──────────────────────────────────────┤
│ Services (internal/services/) │ ← Business logic, orchestration
├──────────────────────────────────────┤
│ Repositories (internal/repositories)│ ← SQL queries, data access
├──────────────────────────────────────┤
│ Models (internal/models/) │ ← Pure data structures
└──────────────────────────────────────┘
Why this pattern: Each layer can be tested in isolation with mocks/stubs for the layer below. Handlers never touch SQL, repositories never contain business logic, models carry no behaviour.
Dependency Injection
Wiring happens bottom-up in cmd/api/main.go → buildHTTPServer():
repositories → services → server
The Server struct holds service interfaces, services hold repository interfaces. All wired at startup, no runtime lookups.
Technology Choices
| Technology | Why |
|---|---|
| Gin | Fast HTTP router, middleware ecosystem, mature Go community |
| pgx/v5 | Direct PostgreSQL driver — no ORM overhead, prepared statements, JSONB-native |
| Viper | Unified config from YAML files + environment variables with prefix support |
| slog | Structured logging from Go stdlib — zero external dependency |
| golang-migrate | Embedded SQL migrations compiled into the binary for single-file deploys |
| testify | Assertion helpers and mock generation for clean test code |
Data Models
Relying Party
Represents a Wallet Relying Party (WRP):
id— Unique identifiercountry— ISO 3166-1 alpha-2 country codeis_psb— Public Sector Body flagis_intermediary— Intermediary flagprovider_type— Type of providertrade_names— Array of trade namesidentifiers— Array of{type, identifier}objectsentitlements— Entitlement URIssupport_uris,service_descriptions— Localized metadatauses_intermediaries— IDs of intermediaries usedsupervisory_authority— Country, email, info_uri, phoneprovided_attestations— Attestations with format, meta, and nested claimscreated_at,updated_at— Timestamps
Intended Use
A use case linked to a relying party via wrp_id:
id,wrp_id— Identifiersrelying_party— Optional embedded RP objectspec_created_at,spec_revoked_at— Specification lifecyclepurposes— Localized purpose descriptionspolicies— Policy objects with URI and optional typecredentials— Credential requirements with format, meta, and claimscreated_at,updated_at— Timestamps
For the full database schema, see database-schema-new.md.
API Documentation
The API endpoints are auto-documented at runtime. Start the server and visit /docs for the interactive OpenAPI documentation generated by Huma.
Pagination
All collection endpoints use cursor-based pagination (not offset-based):
- Default page size: 20, max: 100
- Response shape:
{ "data": [...], "next_cursor": "...", "has_more": true } - Pass
?cursor=<next_cursor>to fetch the next page
Why cursor-based: Offset pagination degrades with depth (Postgres still scans skipped rows). Cursors use indexed WHERE clauses, so page 1000 is as fast as page 1. Stable results even when data changes between requests.
Implementation: internal/utils/pagination.go — cursors are opaque base64-encoded tokens.
Database Design
Trigger-Based Filter Indexing
A filter_values table tracks {field_name, value, usage_count} for dynamic filter discovery. PostgreSQL triggers on wrp and intended_use tables automatically maintain these counts on INSERT/UPDATE/DELETE. This powers the /api/v1/filtering-options autocomplete endpoint without expensive aggregation queries at read time.
JSONB Usage
Complex nested structures (attestations, credentials, claims) are stored as JSONB for schema flexibility. Searchable fields are extracted into dedicated indexed columns for filter performance.
Index Types
- B-tree — Exact-match filters (country, is_psb, foreign keys)
- GIN trigram — Fuzzy text search (trade_name, claim paths, meta fields)
- Composite — Optimized multi-column filters (field_name + usage_count + value)
Normalization
Frequently repeated strings (entitlements, identifier types, provider types) are stored in a shared strings table to reduce duplication.
Adding a New Filter Parameter
-
Add database index — Create a migration with the appropriate index type:
- Exact-match:
CREATE INDEX idx_wrp_field ON wrp(field); - Fuzzy text:
CREATE INDEX idx_wrp_field_trgm ON wrp USING GIN (field gin_trgm_ops);
- Exact-match:
-
Add filter trigger (if autocomplete needed) — In the same migration, create a trigger function that calls
upsert_filter_value/decrement_filter_valueon INSERT/UPDATE/DELETE. -
Update filter struct — Add the field to the filter struct in the service layer (e.g.,
RelyingPartyFiltersininternal/services/relying_party.go). -
Update repository — Add a WHERE clause in the repository using the new index.
-
Update handler — Parse the query parameter in the handler (e.g.,
internal/server/handlers_relying_party.go). -
Add tests — Repository (pgx stubs), service (testify mocks), handler (Gin test context).
-
Verify with EXPLAIN ANALYZE — Test the query plan with 1000+ records to confirm index usage.
Adding a New Endpoint
- Define model in
internal/models/if needed - Add repository method with interface in
internal/repositories/interfaces.go - Add service method with interface in
internal/services/interfaces.go - Add handler in
internal/server/handlers_*.go - Register route in
internal/server/server.go→setupRoutes() - Wire dependencies in
cmd/api/main.go→buildHTTPServer()if new repo/service - Add tests at all layers
- Run performance tests —
./scripts/perf-test.sh