This page is for developers and technical staff who want to understand how Observer is built. If you’re an admin setting up Observer for your organization, you can skip this — head to Deployment instead.

High-Level Overview

Every HTTP request flows through the same path: it enters the server, passes through middleware (authentication, logging), reaches a handler that delegates to a use case, and the use case talks to the database through a repository. Configuration and dependency injection wire everything together at startup.

  graph TD
    CLIENT[HTTP Client] --> SERVER[Server<br>internal/server]
    SERVER --> MW[Middleware<br>internal/middleware]
    MW --> HANDLER[Handlers<br>internal/handler]
    HANDLER --> USECASE[Use Cases<br>internal/usecase]
    USECASE --> IFACE[Repository Interfaces<br>internal/domain/*/repository.go]
    IFACE -.implements.-> IMPL[Repository Implementations<br>internal/postgres]
    IMPL --> DB[(PostgreSQL)]
    USECASE --> CRYPTO[Crypto<br>internal/crypto]
    MW --> CRYPTO
    APP[DI Container<br>internal/app] -.wires.-> SERVER
    APP -.wires.-> HANDLER
    APP -.wires.-> USECASE
    APP -.wires.-> IMPL
    CONFIG[Config<br>internal/config] --> APP

Dependency Flow (Clean Architecture)

The codebase is organized in layers. Inner layers define the rules, outer layers provide the infrastructure. Dependencies always point inward — business logic never imports database or HTTP code directly. This makes it possible to test use cases without a running database.

  graph LR
    subgraph Outer["Outer Layer (Infrastructure)"]
        PG[internal/postgres]
        SRV[internal/server]
        CFG[internal/config]
    end

    subgraph Middle["Middle Layer (Adapters)"]
        HDL[internal/handler]
        MDW[internal/middleware]
    end

    subgraph Inner["Inner Layer (Business)"]
        UC[internal/usecase]
    end

    subgraph Core["Core (Domain)"]
        ENT[Entities]
        IFACE[Repository Interfaces]
        ERR[Domain Errors]
    end

    PG -->|implements| IFACE
    HDL -->|calls| UC
    MDW -->|uses| IFACE
    UC -->|depends on| IFACE
    UC -->|uses| ENT
    UC -->|returns| ERR
    HDL -->|maps| ERR

    style Core fill:#e8f5e9
    style Inner fill:#fff3e0
    style Middle fill:#e3f2fd
    style Outer fill:#fce4ec

Repository: Interface to Implementation

Domain code defines what data operations are needed (interfaces), while the PostgreSQL layer provides the how (implementations). This separation means you could swap PostgreSQL for another database without touching any business logic. Each domain area — users, auth, projects, reference data — has its own repository interface.

  classDiagram
    direction LR

    namespace domain_user {
        class UserRepository {
            <<interface>>
            +Create(ctx, *User) error
            +GetByID(ctx, ulid.ULID) (*User, error)
            +GetByEmail(ctx, string) (*User, error)
            +GetByPhone(ctx, string) (*User, error)
            +Update(ctx, *User) error
            +UpdateVerified(ctx, ulid.ULID, bool) error
            +List(ctx, UserListFilter) ([]*User, int, error)
        }
        class CredentialsRepository {
            <<interface>>
            +Create(ctx, *Credentials) error
            +GetByUserID(ctx, ulid.ULID) (*Credentials, error)
        }
        class MFARepository {
            <<interface>>
            +Create(ctx, *MFAConfig) error
            +GetByUserID(ctx, ulid.ULID) (*MFAConfig, error)
        }
    }

    namespace domain_auth {
        class SessionRepository {
            <<interface>>
            +Create(ctx, *Session) error
            +GetByRefreshToken(ctx, string) (*Session, error)
            +Delete(ctx, ulid.ULID) error
            +DeleteByRefreshToken(ctx, string) error
        }
    }

    namespace domain_project {
        class PermissionLoader {
            <<interface>>
            +GetPermission(ctx, ulid.ULID, string) (*Permission, error)
            +IsProjectOwner(ctx, ulid.ULID, string) (bool, error)
        }
        class PermissionRepository {
            <<interface>>
            +List(ctx, string) ([]*ProjectPermission, error)
            +GetByID(ctx, string) (*ProjectPermission, error)
            +Create(ctx, *ProjectPermission) error
            +Update(ctx, *ProjectPermission) error
            +Delete(ctx, string) error
        }
    }

    namespace domain_reference {
        class CountryRepository {
            <<interface>>
            +List(ctx) ([]*Country, error)
            +GetByID(ctx, string) (*Country, error)
            +Create(ctx, *Country) error
            +Update(ctx, *Country) error
            +Delete(ctx, string) error
        }
        class StateRepository {
            <<interface>>
        }
        class PlaceRepository {
            <<interface>>
        }
        class OfficeRepository {
            <<interface>>
        }
        class CategoryRepository {
            <<interface>>
        }
    }

    namespace postgres {
        class pg_UserRepository {
            -db *sqlx.DB
        }
        class pg_CredentialsRepository {
            -db *sqlx.DB
        }
        class pg_SessionRepository {
            -db *sqlx.DB
        }
        class pg_MFARepository {
            -db *sqlx.DB
        }
        class pg_PermissionRepository {
            -db *sqlx.DB
        }
        class pg_ProjectPermissionRepository {
            -db *sqlx.DB
        }
        class pg_CountryRepository {
            -db *sqlx.DB
        }
    }

    pg_UserRepository ..|> UserRepository
    pg_CredentialsRepository ..|> CredentialsRepository
    pg_SessionRepository ..|> SessionRepository
    pg_MFARepository ..|> MFARepository
    pg_PermissionRepository ..|> PermissionLoader
    pg_ProjectPermissionRepository ..|> PermissionRepository
    pg_CountryRepository ..|> CountryRepository

Use Cases: Who Depends on What

Each user action — logging in, listing people, assigning permissions — is handled by a dedicated use case. Use cases coordinate between repositories and crypto services but contain no HTTP or database code themselves. The diagram below shows which repositories each use case depends on.

  graph TD
    subgraph auth_usecases["Auth Use Cases"]
        REG[RegisterUseCase]
        LOG[LoginUseCase]
        REF[RefreshTokenUseCase]
        OUT[LogoutUseCase]
    end

    subgraph admin_usecases["Admin Use Cases"]
        LU[ListUsersUseCase]
        GU[GetUserUseCase]
        UU[UpdateUserUseCase]
    end

    subgraph perm_usecases["Permission Use Cases"]
        LP[ListPermissionsUseCase]
        AP[AssignPermissionUseCase]
        UP[UpdatePermissionUseCase]
        RP[RevokePermissionUseCase]
    end

    subgraph ref_usecases["Reference Use Cases"]
        COU[CountryUseCase]
        STA[StateUseCase]
        PLA[PlaceUseCase]
        OFF[OfficeUseCase]
        CAT[CategoryUseCase]
    end

    subgraph interfaces["Repository Interfaces"]
        UR[UserRepository]
        CR[CredentialsRepository]
        SR[SessionRepository]
        MR[MFARepository]
        PR[PermissionRepository]
        COR[CountryRepository]
        STR[StateRepository]
        PLR[PlaceRepository]
        OFR[OfficeRepository]
        CAR[CategoryRepository]
    end

    subgraph crypto["Crypto Services"]
        PH[PasswordHasher]
        TG[TokenGenerator]
    end

    REG --> UR
    REG --> CR
    REG --> PH

    LOG --> UR
    LOG --> CR
    LOG --> SR
    LOG --> MR
    LOG --> PH
    LOG --> TG

    REF --> SR
    REF --> TG

    OUT --> SR

    LU --> UR
    GU --> UR
    UU --> UR

    LP --> PR
    AP --> PR
    UP --> PR
    RP --> PR

    COU --> COR
    STA --> STR
    PLA --> PLR
    OFF --> OFR
    CAT --> CAR

HTTP Request Flow

Here’s what happens when a user logs in. The request enters through the router, passes through middleware that assigns a request ID and logger, then reaches the auth handler. The handler parses the JSON body and calls the login use case, which looks up the user, verifies the password with Argon2, generates JWT tokens, and creates a session.

  sequenceDiagram
    participant C as Client
    participant R as Router
    participant MW as Middleware
    participant H as Handler
    participant UC as UseCase
    participant Repo as Repository
    participant DB as PostgreSQL

    C->>R: POST /auth/login
    R->>MW: requestID + logger + recovery
    MW->>H: AuthHandler.Login
    H->>H: Bind JSON request
    H->>UC: LoginUseCase.Execute(input)
    UC->>Repo: UserRepo.GetByEmail(email)
    Repo->>DB: SELECT ... FROM users
    DB-->>Repo: row
    Repo-->>UC: *User
    UC->>Repo: CredRepo.GetByUserID(id)
    Repo->>DB: SELECT ... FROM credentials
    DB-->>Repo: row
    Repo-->>UC: *Credentials
    UC->>UC: Verify password (Argon2)
    UC->>UC: Generate tokens (RSA)
    UC->>Repo: SessionRepo.Create(session)
    Repo->>DB: INSERT INTO sessions
    UC-->>H: LoginOutput
    H-->>C: 200 JSON response

Protected Route Flow (Admin + Project RBAC)

Protected routes go through additional checks. Admin routes verify the user’s platform role (admin, staff, etc.). Project-scoped routes load the user’s project-level permission and check whether their project role is sufficient for the requested action. The middleware also sets sensitivity flags that control whether the response includes contact info, personal details, or document data.

  sequenceDiagram
    participant C as Client
    participant R as Router
    participant Auth as AuthMiddleware
    participant Role as RequireRole
    participant ProjAuth as ProjectAuthMiddleware
    participant H as Handler
    participant UC as UseCase

    C->>R: GET /admin/projects/:id/permissions
    R->>Auth: Authenticate()
    Auth->>Auth: Parse Bearer JWT
    Auth->>Auth: Set CtxUserID, CtxUserRole
    Auth-->>Role: next
    Role->>Role: Check user.Role in [admin]
    Role-->>H: next (or 403)
    H->>UC: ListPermissionsUseCase.Execute(projectID)
    UC-->>H: []PermissionDTO
    H-->>C: 200 JSON

    Note over C,R: Project-scoped route (future)
    C->>R: GET /projects/:id/people
    R->>Auth: Authenticate()
    Auth-->>ProjAuth: next
    ProjAuth->>ProjAuth: Load PermissionLoader.GetPermission()
    ProjAuth->>ProjAuth: Check role rank >= MinRoleForAction
    ProjAuth->>ProjAuth: Set CtxProjectRole + sensitivity flags
    ProjAuth-->>H: next (or 403)

DI Container Wiring

At startup, the application reads configuration and connects to the database, then wires everything together in a dependency injection container. The container creates repositories, crypto services, and use cases, passing each component its dependencies. The fully assembled container is handed to the server, which injects handlers and middleware into the router.

  graph TD
    subgraph inputs["Inputs"]
        CFG[Config]
        DB[Database]
    end

    subgraph container["Container wires everything"]
        direction TB
        KEYS[RSA Keys] --> TG[TokenGenerator]
        HASH[ArgonHasher]

        DB --> |sqlxDB| REPOS[All Repositories]

        REPOS --> UC_AUTH[Auth Use Cases]
        HASH --> UC_AUTH
        TG --> UC_AUTH

        REPOS --> UC_ADMIN[Admin Use Cases]
        REPOS --> UC_REF[Reference Use Cases]
        REPOS --> UC_PERM[Permission Use Cases]
    end

    subgraph output["Output"]
        CONT[Container struct]
    end

    CFG --> KEYS
    CFG --> container
    DB --> container
    container --> CONT

    CONT --> SERVER[Server.setupRoutes]
    SERVER --> |injects into| HANDLERS[Handlers]
    SERVER --> |injects into| MIDDLEWARE[Middleware]