Architecture
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]