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.gobuildHTTPServer():

repositories → services → server

The Server struct holds service interfaces, services hold repository interfaces. All wired at startup, no runtime lookups.

Technology Choices

TechnologyWhy
GinFast HTTP router, middleware ecosystem, mature Go community
pgx/v5Direct PostgreSQL driver — no ORM overhead, prepared statements, JSONB-native
ViperUnified config from YAML files + environment variables with prefix support
slogStructured logging from Go stdlib — zero external dependency
golang-migrateEmbedded SQL migrations compiled into the binary for single-file deploys
testifyAssertion helpers and mock generation for clean test code

Data Models

Relying Party

Represents a Wallet Relying Party (WRP):

  • id — Unique identifier
  • country — ISO 3166-1 alpha-2 country code
  • is_psb — Public Sector Body flag
  • is_intermediary — Intermediary flag
  • provider_type — Type of provider
  • trade_names — Array of trade names
  • identifiers — Array of {type, identifier} objects
  • entitlements — Entitlement URIs
  • support_uris, service_descriptions — Localized metadata
  • uses_intermediaries — IDs of intermediaries used
  • supervisory_authority — Country, email, info_uri, phone
  • provided_attestations — Attestations with format, meta, and nested claims
  • created_at, updated_at — Timestamps

Intended Use

A use case linked to a relying party via wrp_id:

  • id, wrp_id — Identifiers
  • relying_party — Optional embedded RP object
  • spec_created_at, spec_revoked_at — Specification lifecycle
  • purposes — Localized purpose descriptions
  • policies — Policy objects with URI and optional type
  • credentials — Credential requirements with format, meta, and claims
  • created_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

  1. 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);
  2. Add filter trigger (if autocomplete needed) — In the same migration, create a trigger function that calls upsert_filter_value / decrement_filter_value on INSERT/UPDATE/DELETE.

  3. Update filter struct — Add the field to the filter struct in the service layer (e.g., RelyingPartyFilters in internal/services/relying_party.go).

  4. Update repository — Add a WHERE clause in the repository using the new index.

  5. Update handler — Parse the query parameter in the handler (e.g., internal/server/handlers_relying_party.go).

  6. Add tests — Repository (pgx stubs), service (testify mocks), handler (Gin test context).

  7. Verify with EXPLAIN ANALYZE — Test the query plan with 1000+ records to confirm index usage.

Adding a New Endpoint

  1. Define model in internal/models/ if needed
  2. Add repository method with interface in internal/repositories/interfaces.go
  3. Add service method with interface in internal/services/interfaces.go
  4. Add handler in internal/server/handlers_*.go
  5. Register route in internal/server/server.gosetupRoutes()
  6. Wire dependencies in cmd/api/main.gobuildHTTPServer() if new repo/service
  7. Add tests at all layers
  8. Run performance tests./scripts/perf-test.sh