A Go reimplementation of the Immich backend API, designed for cloud-native deployments with first-class S3, embedded PostgreSQL for single-binary demos, and a clean gRPC + REST architecture.
[!IMPORTANT] Status: experimental preview, not production-ready. The project has broad API coverage and is useful for development and disposable demos, but several workflows still have placeholder semantics and recovery, multi-replica state, and end-to-end storage consistency remain incomplete. It has not undergone an independent security review. Do not rely on it for the only copy of a photo library or expose it to untrusted users yet.
The official Immich backend is a battle-tested NestJS stack. This project explores an alternative with:
sqlc/queries.sql — no ORM, no string-interpolated SQL.flowchart LR
subgraph Clients
Web[Immich Web<br/>SvelteKit]
iOS[Immich iOS]
Android[Immich Android]
CLI[immich CLI / API users]
end
subgraph Binary [immich-go-backend binary]
REST[REST gateway<br/>:3001 from config.yaml]
GRPC[gRPC server<br/>:3002 from config.yaml]
subgraph Services [34 gRPC services]
Auth[Auth]
Assets[Assets]
Albums[Albums]
Sync[Sync]
Others[Search · People · Memories<br/>Timeline · Tags · Jobs · ...]
end
Storage[Storage abstraction]
Queue[asynq job queue]
WS[WebSocket hub]
end
PG[(PostgreSQL + VectorChord)]
PGembed[(Embedded PG<br/>demo only)]
S3[(S3 / Rclone / Local)]
Redis[(Redis 7)]
WebUI[Static web bundle]
Web -- HTTPS --> REST
iOS --> REST
Android --> REST
CLI --> REST
Web -.transport handshake.-> WS
REST --> GRPC
GRPC --> Services
Services --> Storage
Services --> Queue
Services --> PG
PGembed -. replace .-> PG
Storage --> S3
Queue --> Redis
REST -. fallback .-> WebUI
A REST request hits grpc-gateway on :3001 (the value in config.yaml — the Go default is 8080), is translated to a gRPC call against the matching service, the service runs the business logic against PostgreSQL via SQLC queries, and reads/writes bytes through the storage backend. Long-running work (thumbnail generation, metadata extraction, ML indexing) is enqueued onto Redis via asynq.
For the full architecture — service dependency graph, storage backends, auth context, embedded PG boot sequence, and job flow — see ARCHITECTURE.md.
export AUTH_JWT_SECRET="$(openssl rand -hex 32)"
docker compose up -d --wait
./bin/immich-go-backend migrate
./bin/immich-go-backend serve
The default docker-compose.yml starts Redis 7 and the digest-pinned PostgreSQL 14 + VectorChord image used by the checked-out Immich v3.0.1 release. That image provides the uuid-ossp, vector, vchord, cube, earthdistance, pg_trgm, and unaccent extensions required by this backend's migrations; the database health check verifies their availability. A stock postgres image cannot run a clean install. serve requires a random JWT secret of at least 32 bytes; keep it stable across restarts so existing sessions remain valid. The server listens on :3001 (REST) and :3002 (gRPC) per config.yaml — the Go default in internal/config/config.go is 8080/9090 and config.yaml overrides it.
Open http://localhost:3001/api/server/ping to confirm the server is up, then POST /api/auth/admin-sign-up to create the first admin.
The fastest way to a public preview. See DEPLOYMENT.md → Fly.io single-machine demo.
fly apps create immich-go-demo
fly volumes create immich_data --size 10 --region iad
fly secrets set AUTH_JWT_SECRET="$(openssl rand -hex 32)"
fly deploy
# → https://immich-go-demo.fly.dev
The image bundles the official Immich web build and starts an embedded PostgreSQL inside the binary, so the only persistent state is a single Fly volume.
A demo instance runs at https://immich-go-backend.fly.dev. It is redeployed from master on every green CI run and wiped on each deploy (IMMICH_DEMO_FRESH_ON_DEPLOY), then verified by the Playwright E2E suite. The suite registers the root admin on the fresh instance:
e2e-root-admin@example.comE2ePassword123!Anything you upload there is public and disappears on the next deploy.
nix develop # pinned toolchain (Go, buf, sqlc, golangci-lint)
make setup # generate protos + tidy modules
make build # build ./bin/immich-go-backend
make test # unit + integration tests (needs Docker)
make lint # golangci-lint
All targets are described in the Makefile. Generated proto code under internal/proto/gen/ is ignored; make build/make test, Docker builds, and CI run buf generate before compiling.
cmd/ CLI entry (Cobra): serve / migrate / version
internal/
server/ Wires all services, gRPC server, REST gateway
<service>/ One package per gRPC service (assets, albums, ...)
proto/ .proto sources + ignored generated Go (gen/)
db/ Database connection, embed.FS migrations
db/sqlc/ SQLC-generated Go (do not edit)
db/testdb/ testcontainers helpers for integration tests
storage/ local / s3 / rclone backends behind one interface
jobs/ asynq service + handlers
embedded/ embedded PostgreSQL for demo mode
webui/ Static-file fallback for the bundled web UI
websocket/ Live event hub
telemetry/ OpenTelemetry traces + metrics
auth/ JWT, sessions, rate limiter, middleware
config/ YAML + env-var configuration
sqlc/
schema.sql Database schema (43 tables)
queries.sql 240 SQLC query definitions
deploy/ Dockerfile + fly.toml + scripts
e2e/ Frontend Playwright tests (upload, view, ...)
The server registers 34 gRPC services (~222 RPCs) against internal/proto/:
| Group | Services (gRPC Service names) |
|---|---|
| Identity & access | AuthService, OAuthService, UsersService, ApiKeyService, SessionsService, SharedLinksService, PartnersService |
| Library & assets | AssetService, AlbumService, LibrariesService, DownloadService, StacksService, TagsService, TrashService |
| Discovery | SearchService, TimelineService, MemoryService, MapService, PeopleService, FacesService, DuplicatesService, ViewService, ActivityService |
| System | ServerService, JobService, QueueService, MaintenanceService, SystemConfigService, SystemMetadataService, SyncService, NotificationsService, PluginService, WorkflowService, AdminService |
Each service has the same shape: *sqlc.Queries + *config.Config + the dependencies it actually needs (storage, sync, queue). See ARCHITECTURE.md → Service layer for the dependency graph.
config.yaml is the template. Most fields are overridden by unprefixed environment variables — the pattern is <SECTION>_<KEY> in upper snake case, mirroring the YAML path (e.g. server.address → SERVER_ADDRESS, database.url → DATABASE_URL, auth.jwt_secret → AUTH_JWT_SECRET). The exceptions use an IMMICH_ prefix: IMMICH_WEBUI_DIR, IMMICH_DATABASE_AUTO_MIGRATE, and IMMICH_EMBEDDED_DB. The authoritative list lives in the struct tags of internal/config/config.go.
Most useful sections:
| Section | Purpose |
|---|---|
server | HTTP/gRPC bind addresses, timeouts, CORS, metrics path |
database | DSN, pool sizing, auto-migrate flag |
storage | Backend (local / s3 / rclone); pre-signed URLs (S3 only), upload limits |
auth | JWT secret/expiry, registration toggle, password policy, login rate-limit. serve requires a non-placeholder secret of at least 32 bytes |
jobs | asynq Redis URL, worker count |
telemetry | OpenTelemetry tracing/metrics toggles, sampling rate |
features | Boolean flags (FEATURE_MACHINE_LEARNING_ENABLED, FEATURE_FACE_RECOGNITION_ENABLED, FEATURE_CLIP_SEARCH_ENABLED, FEATURE_VIDEO_TRANSCODING_ENABLED, FEATURE_THUMBNAIL_GENERATION_ENABLED, FEATURE_EXIF_EXTRACTION_ENABLED, FEATURE_DUPLICATE_DETECTION_ENABLED, FEATURE_BACKUP_SYNC_ENABLED, FEATURE_SHARING_ENABLED, …) |
For the full env-var reference and the precedence rules, see DEPLOYMENT.md → Configuration.
make proto-gen — regenerate internal/proto/gen/ from internal/proto/*.protomake sqlc-gen — regenerate internal/db/sqlc/ from sqlc/queries.sqlmake build / make test / make lint / make ci-checkgo test -v -run TestName ./internal/auth/...The linter is golangci-lint with errcheck, ineffassign, staticcheck, unused, gosec, govet, gofmt, gofumpt, goimports, misspell, dogsled, nakedret, gocritic, gosimple, unconvert, and copyloopvar. Generated files are excluded.
CI (.github/workflows/go.yaml) runs on Go 1.26.5 + buf 1.49.0 with two parallel jobs: build (unit tests under -short, lint, vet) and integration-tests (go test -tags=integration -race over ./internal/...). Per-package shard files live under .github/test-shards/ and are not yet consumed by the workflow.
Two layers:
Unit tests run anywhere with go test ./....
Integration tests spin up a real PostgreSQL via testcontainers. They require the integration build tag:
go test -tags=integration ./internal/db/...
go test -tags=integration ./internal/auth/...
The shared helper is internal/db/testdb.SetupTestDB(). See TESTING.md.
Two Dockerfiles cover the deployment matrix:
Dockerfile — base image (external Postgres/Redis, no web bundle). Used for Docker Compose, Kubernetes, plain Docker, and the ghcr.io/<repo> release images.Dockerfile.fly — Fly.io demo image (embedded Postgres, web bundle baked in at /app/web, tini for SIGTERM forwarding). Selected via fly.toml.The base image produces a static binary; the Fly image adds the embedded PostgreSQL runtime and the official Immich web build on top. Deployment guides:
CI currently gates route coverage against the upstream Immich v3.0.1 OpenAPI document. Route coverage is not a claim of behavioral parity; see ROADMAP.md for the remaining semantic and production-readiness work.
Read CONTRIBUTING.md for code conventions, the "adding a new service" walkthrough, and how we use conventional commits.
AGPL-3.0, matching upstream Immich. This project is an independent reimplementation and is not affiliated with or endorsed by the Immich project.
Go
88.8%
PLpgSQL
6.6%
TypeScript
3.2%
A Go reimplementation of the Immich backend API, designed for cloud-native deployments with first-class S3, embedded PostgreSQL for single-binary demos, and a clean gRPC + REST architecture.
[!IMPORTANT] Status: experimental preview, not production-ready. The project has broad API coverage and is useful for development and disposable demos, but several workflows still have placeholder semantics and recovery, multi-replica state, and end-to-end storage consistency remain incomplete. It has not undergone an independent security review. Do not rely on it for the only copy of a photo library or expose it to untrusted users yet.
The official Immich backend is a battle-tested NestJS stack. This project explores an alternative with:
sqlc/queries.sql — no ORM, no string-interpolated SQL.flowchart LR
subgraph Clients
Web[Immich Web<br/>SvelteKit]
iOS[Immich iOS]
Android[Immich Android]
CLI[immich CLI / API users]
end
subgraph Binary [immich-go-backend binary]
REST[REST gateway<br/>:3001 from config.yaml]
GRPC[gRPC server<br/>:3002 from config.yaml]
subgraph Services [34 gRPC services]
Auth[Auth]
Assets[Assets]
Albums[Albums]
Sync[Sync]
Others[Search · People · Memories<br/>Timeline · Tags · Jobs · ...]
end
Storage[Storage abstraction]
Queue[asynq job queue]
WS[WebSocket hub]
end
PG[(PostgreSQL + VectorChord)]
PGembed[(Embedded PG<br/>demo only)]
S3[(S3 / Rclone / Local)]
Redis[(Redis 7)]
WebUI[Static web bundle]
Web -- HTTPS --> REST
iOS --> REST
Android --> REST
CLI --> REST
Web -.transport handshake.-> WS
REST --> GRPC
GRPC --> Services
Services --> Storage
Services --> Queue
Services --> PG
PGembed -. replace .-> PG
Storage --> S3
Queue --> Redis
REST -. fallback .-> WebUI
A REST request hits grpc-gateway on :3001 (the value in config.yaml — the Go default is 8080), is translated to a gRPC call against the matching service, the service runs the business logic against PostgreSQL via SQLC queries, and reads/writes bytes through the storage backend. Long-running work (thumbnail generation, metadata extraction, ML indexing) is enqueued onto Redis via asynq.
For the full architecture — service dependency graph, storage backends, auth context, embedded PG boot sequence, and job flow — see ARCHITECTURE.md.
export AUTH_JWT_SECRET="$(openssl rand -hex 32)"
docker compose up -d --wait
./bin/immich-go-backend migrate
./bin/immich-go-backend serve
The default docker-compose.yml starts Redis 7 and the digest-pinned PostgreSQL 14 + VectorChord image used by the checked-out Immich v3.0.1 release. That image provides the uuid-ossp, vector, vchord, cube, earthdistance, pg_trgm, and unaccent extensions required by this backend's migrations; the database health check verifies their availability. A stock postgres image cannot run a clean install. serve requires a random JWT secret of at least 32 bytes; keep it stable across restarts so existing sessions remain valid. The server listens on :3001 (REST) and :3002 (gRPC) per config.yaml — the Go default in internal/config/config.go is 8080/9090 and config.yaml overrides it.
Open http://localhost:3001/api/server/ping to confirm the server is up, then POST /api/auth/admin-sign-up to create the first admin.
The fastest way to a public preview. See DEPLOYMENT.md → Fly.io single-machine demo.
fly apps create immich-go-demo
fly volumes create immich_data --size 10 --region iad
fly secrets set AUTH_JWT_SECRET="$(openssl rand -hex 32)"
fly deploy
# → https://immich-go-demo.fly.dev
The image bundles the official Immich web build and starts an embedded PostgreSQL inside the binary, so the only persistent state is a single Fly volume.
A demo instance runs at https://immich-go-backend.fly.dev. It is redeployed from master on every green CI run and wiped on each deploy (IMMICH_DEMO_FRESH_ON_DEPLOY), then verified by the Playwright E2E suite. The suite registers the root admin on the fresh instance:
e2e-root-admin@example.comE2ePassword123!Anything you upload there is public and disappears on the next deploy.
nix develop # pinned toolchain (Go, buf, sqlc, golangci-lint)
make setup # generate protos + tidy modules
make build # build ./bin/immich-go-backend
make test # unit + integration tests (needs Docker)
make lint # golangci-lint
All targets are described in the Makefile. Generated proto code under internal/proto/gen/ is ignored; make build/make test, Docker builds, and CI run buf generate before compiling.
cmd/ CLI entry (Cobra): serve / migrate / version
internal/
server/ Wires all services, gRPC server, REST gateway
<service>/ One package per gRPC service (assets, albums, ...)
proto/ .proto sources + ignored generated Go (gen/)
db/ Database connection, embed.FS migrations
db/sqlc/ SQLC-generated Go (do not edit)
db/testdb/ testcontainers helpers for integration tests
storage/ local / s3 / rclone backends behind one interface
jobs/ asynq service + handlers
embedded/ embedded PostgreSQL for demo mode
webui/ Static-file fallback for the bundled web UI
websocket/ Live event hub
telemetry/ OpenTelemetry traces + metrics
auth/ JWT, sessions, rate limiter, middleware
config/ YAML + env-var configuration
sqlc/
schema.sql Database schema (43 tables)
queries.sql 240 SQLC query definitions
deploy/ Dockerfile + fly.toml + scripts
e2e/ Frontend Playwright tests (upload, view, ...)
The server registers 34 gRPC services (~222 RPCs) against internal/proto/:
| Group | Services (gRPC Service names) |
|---|---|
| Identity & access | AuthService, OAuthService, UsersService, ApiKeyService, SessionsService, SharedLinksService, PartnersService |
| Library & assets | AssetService, AlbumService, LibrariesService, DownloadService, StacksService, TagsService, TrashService |
| Discovery | SearchService, TimelineService, MemoryService, MapService, PeopleService, FacesService, DuplicatesService, ViewService, ActivityService |
| System | ServerService, JobService, QueueService, MaintenanceService, SystemConfigService, SystemMetadataService, SyncService, NotificationsService, PluginService, WorkflowService, AdminService |
Each service has the same shape: *sqlc.Queries + *config.Config + the dependencies it actually needs (storage, sync, queue). See ARCHITECTURE.md → Service layer for the dependency graph.
config.yaml is the template. Most fields are overridden by unprefixed environment variables — the pattern is <SECTION>_<KEY> in upper snake case, mirroring the YAML path (e.g. server.address → SERVER_ADDRESS, database.url → DATABASE_URL, auth.jwt_secret → AUTH_JWT_SECRET). The exceptions use an IMMICH_ prefix: IMMICH_WEBUI_DIR, IMMICH_DATABASE_AUTO_MIGRATE, and IMMICH_EMBEDDED_DB. The authoritative list lives in the struct tags of internal/config/config.go.
Most useful sections:
| Section | Purpose |
|---|---|
server | HTTP/gRPC bind addresses, timeouts, CORS, metrics path |
database | DSN, pool sizing, auto-migrate flag |
storage | Backend (local / s3 / rclone); pre-signed URLs (S3 only), upload limits |
auth | JWT secret/expiry, registration toggle, password policy, login rate-limit. serve requires a non-placeholder secret of at least 32 bytes |
jobs | asynq Redis URL, worker count |
telemetry | OpenTelemetry tracing/metrics toggles, sampling rate |
features | Boolean flags (FEATURE_MACHINE_LEARNING_ENABLED, FEATURE_FACE_RECOGNITION_ENABLED, FEATURE_CLIP_SEARCH_ENABLED, FEATURE_VIDEO_TRANSCODING_ENABLED, FEATURE_THUMBNAIL_GENERATION_ENABLED, FEATURE_EXIF_EXTRACTION_ENABLED, FEATURE_DUPLICATE_DETECTION_ENABLED, FEATURE_BACKUP_SYNC_ENABLED, FEATURE_SHARING_ENABLED, …) |
For the full env-var reference and the precedence rules, see DEPLOYMENT.md → Configuration.
make proto-gen — regenerate internal/proto/gen/ from internal/proto/*.protomake sqlc-gen — regenerate internal/db/sqlc/ from sqlc/queries.sqlmake build / make test / make lint / make ci-checkgo test -v -run TestName ./internal/auth/...The linter is golangci-lint with errcheck, ineffassign, staticcheck, unused, gosec, govet, gofmt, gofumpt, goimports, misspell, dogsled, nakedret, gocritic, gosimple, unconvert, and copyloopvar. Generated files are excluded.
CI (.github/workflows/go.yaml) runs on Go 1.26.5 + buf 1.49.0 with two parallel jobs: build (unit tests under -short, lint, vet) and integration-tests (go test -tags=integration -race over ./internal/...). Per-package shard files live under .github/test-shards/ and are not yet consumed by the workflow.
Two layers:
Unit tests run anywhere with go test ./....
Integration tests spin up a real PostgreSQL via testcontainers. They require the integration build tag:
go test -tags=integration ./internal/db/...
go test -tags=integration ./internal/auth/...
The shared helper is internal/db/testdb.SetupTestDB(). See TESTING.md.
Two Dockerfiles cover the deployment matrix:
Dockerfile — base image (external Postgres/Redis, no web bundle). Used for Docker Compose, Kubernetes, plain Docker, and the ghcr.io/<repo> release images.Dockerfile.fly — Fly.io demo image (embedded Postgres, web bundle baked in at /app/web, tini for SIGTERM forwarding). Selected via fly.toml.The base image produces a static binary; the Fly image adds the embedded PostgreSQL runtime and the official Immich web build on top. Deployment guides:
CI currently gates route coverage against the upstream Immich v3.0.1 OpenAPI document. Route coverage is not a claim of behavioral parity; see ROADMAP.md for the remaining semantic and production-readiness work.
Read CONTRIBUTING.md for code conventions, the "adding a new service" walkthrough, and how we use conventional commits.
AGPL-3.0, matching upstream Immich. This project is an independent reimplementation and is not affiliated with or endorsed by the Immich project.
Go
88.8%
PLpgSQL
6.6%
TypeScript
3.2%