- Go 90.7%
- HTML 7.5%
- Makefile 1.3%
- Dockerfile 0.4%
| .forgejo/workflows | ||
| cmd | ||
| docs/superpowers/specs | ||
| internal | ||
| migrations | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| Dockerfile | ||
| entrypoint.sh | ||
| go.mod | ||
| go.sum | ||
| Makefile | ||
| README.md | ||
Goartesory
Modern image gallery web application built in Go with SQLite, S3-compatible storage, and Bootstrap 5.
Features
Public Gallery
- Masonry grid layout with responsive design (any screen size)
- Tag-based filtering: click tags to filter images
- Image detail page with full metadata (title, description, dimensions, tags)
- Pagination: 20 images per page
- Image proxy: images served through Go app from S3 (with caching headers)
- CDN support: rewrite image URLs to CDN domain when configured
Admin Dashboard
- Secure login with bcrypt password hashing (cost 12) and rate limiting
- Role-based access: Admin (full CRUD) and Viewer (read-only)
- Image management: upload, edit metadata, assign tags, delete
- Automatic variants: micro (160x160) and mini (350x350) generated on upload
- Tag management: create, edit, delete tags (CRUD with image count)
- User management: Admin can create/edit/delete users (Admin only)
- CSRF protection on all state-changing requests
- Session management stored in SQLite
Image Processing
- Upload validation (type: JPEG, PNG, WebP, GIF; max 20MB)
- SHA256 checksums for integrity
- Automatic variant generation using libvips (via bimg)
- Variants preserve aspect ratio without cropping or distortion
- Metadata extraction (width, height)
- Images stored in S3-compatible storage (MinIO, AWS S3, Backblaze B2, etc.)
System Requirements
| Requirement | Version |
|---|---|
| Go | 1.26+ |
| golang-migrate CLI | 4.15+ |
| libvips | any recent version |
Installing libvips
# Ubuntu / Debian
sudo apt install libvips-dev
# macOS
brew install vips
# Arch Linux
sudo pacman -S libvips
Installing golang-migrate CLI
go install -tags 'sqlite' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
The
-tags 'sqlite'flag includes the pure-Go SQLite driver (modernc.org/sqlite, no CGO needed).
Quick Start (Development)
# 1. Clone and enter project
git clone git.jesusmarin.dev/angel/goartesory
cd goartesory
# 2. Configure environment
cp .env.example .env
# Edit .env with your values (see Configuration section below)
# 3. Full setup (build + migrate + create admin)
make dev
# 4. Start the server
make run
The app will be available at http://localhost:8080.
Important: The Makefile automatically loads environment variables from
.env. You do NOT need to manuallysource .envorexportvariables. Everything is handled bymake.
Configuration (.env)
All configuration is done via environment variables. Copy .env.example to .env and adjust:
| Variable | Default | Description |
|---|---|---|
PORT |
8080 |
HTTP server port |
ENV |
development |
development or production |
BASE_URL |
http://localhost:8080 |
Public base URL |
DATABASE_PATH |
./data/goartesory.db |
SQLite database file path |
SESSION_SECRET |
required | 64-char random string for session encryption |
SESSION_LIFETIME |
24h |
Session cookie duration |
S3_ENDPOINT |
(optional) | S3-compatible endpoint URL (required for image uploads) |
S3_REGION |
us-east-1 |
S3 region |
S3_BUCKET |
goartesory |
S3 bucket name |
S3_ACCESS_KEY |
(optional) | S3 access key (required for image uploads) |
S3_SECRET_KEY |
(optional) | S3 secret key (required for image uploads) |
S3_USE_PATH_STYLE |
true |
Use path-style addressing (required for MinIO) |
CDN_BASE_URL |
(empty) | CDN base URL for image URLs (optional) |
CSRF_SECRET |
required | 32-byte random string for CSRF protection |
Generating secrets
# Session secret (64 chars)
openssl rand -base64 48
# CSRF secret (32 bytes)
openssl rand -base64 32
Makefile Commands
| Command | Description |
|---|---|
make dev |
Full setup: build binaries, run migrations, create admin user |
make build |
Build both binaries (goartesory + goartesory-setup) |
make run |
Start the web server |
make setup |
Create admin user (interactive CLI) |
make migrate-up |
Run all pending migrations |
make migrate-down |
Rollback all migrations |
make migrate-new |
Create a new migration file |
make reset |
Clean database + binaries, then recreate from scratch |
make clean |
Remove bin/ and data/ directories |
make vet |
Run Go static analysis |
make fmt |
Format Go source code |
make test |
Run test suite (without libvips-dependent tests) |
make test-race |
Run tests with race detector |
make test-cover |
Run tests with coverage report |
make test-all |
Run tests with race detector and coverage report |
Typical Development Workflow
# First time
make dev # build + migrate + create admin
make run # start server
# After pulling changes
make build # rebuild binaries
make migrate-up # apply new migrations
make run # restart
# Reset everything
make reset # clean + build + migrate + setup
Testing
The test suite uses only Go's built-in testing package and net/http/httptest. No external test frameworks.
Summary
| Metric | Value |
|---|---|
| Test files | 34 |
| Total test functions | 243 |
| Strategy | In-memory SQLite repos + hand-written fakes + httptest handlers |
Test Structure
internal/
images/
domain/ errors_test.go # Sentinel error values
application/
service_test.go # ImageService (Create, Update, Delete, ServeProxy, validators)
queue_test.go # VariantQueue lifecycle
bimg_timeout_test.go # CGO timeout wrappers (build tag: !nobimg)
infrastructure/
sqlite/
image_repo_test.go # Real in-memory SQLite: CRUD, tags, pagination
blob_repo_test.go # Real in-memory SQLite: CRUD, unique key
s3/
blob_store_adapter_test.go # Adapter delegation to storage.BlobStore
http/
handler_test.go # Gallery, detail, dashboard CRUD, proxy
routes_test.go # Route registration verification
users/
domain/ user_test.go, errors_test.go # IsAdmin(), sentinel errors
application/ auth_test.go # Authenticate, CreateUser, UpdateUser (bcrypt)
infrastructure/
sqlite/ user_repo_test.go # Real in-memory SQLite: CRUD, UNIQUE email
http/
handler_test.go # Login, logout, user CRUD, self-role/self-delete guards
routes_test.go # Route registration: login public, logout auth
tags/
application/ service_test.go # TagService: CRUD, slug generation
infrastructure/
sqlite/ tag_repo_test.go # Real in-memory SQLite: CRUD, FindAllWithCount
http/
handler_test.go # Tag CRUD with flash messages
routes_test.go # Route registration
storage/
domain/ errors_test.go # Sentinel error values
infrastructure/
s3/ s3_store_test.go # URL generation (cdn/no-cdn)
platform/
config/ config_test.go # Load() from env, defaults, validation
database/
sqlite_test.go # Open(), pragmas, OpenTestDB()
testdb.go # Test helper: in-memory SQLite with full schema
middleware/
auth_test.go # RequireAuth, RequireAdmin, context helpers
csrf_test.go # Token generation, GET/POST validation
logging_test.go # realIP extraction, responseWriter
recovery_test.go # Panic recovery
security_test.go # Security headers
ratelimit/
limiter_test.go # Rate limiter middleware
sqlite_store_test.go # SQLite-backed UPSERT (including concurrent)
response/
errors_test.go # NotFound, ServerError
pages_test.go # Render, RenderLogin, RenderDashboard, SetFlash
filesystem/ templates_test.go # ParseTemplates, FuncMap
router/ router_test.go # New() ServeMux
testutil/ mocks.go # Hand-written fakes for all domain interfaces
Coverage
| Layer | Packages | Coverage |
|---|---|---|
| Domain | users, images, storage, tags | 100% (entity methods + sentinel errors) |
| Application | users, tags, images | 89-100% (services + validators) |
| Infrastructure SQLite | users, images, tags | 84-86% (repos against in-memory DB) |
| Infrastructure HTTP | users, images, tags | 79-97% (handlers via httptest) |
| Platform | config, middleware, response, etc. | 77-100% |
What Is NOT Tested (and why)
| Area | Reason |
|---|---|
cmd/web/main.go, cmd/setup/main.go |
main() entry points — tested via integration/manual testing |
ImageService.Create() image processing path |
Depends on h2non/bimg (CGO/libvips). Tests use -tags nobimg to skip. Pre-processing validations (type, size) are still tested. |
VariantQueue.processJob() bimg calls |
Same as above — CGO dependency not available in all environments. |
storage/infrastructure/s3/ S3 operations |
Requires a real S3-compatible endpoint. Only URL() method is tested. |
testutil/mocks.go |
Test helpers — self-testing is redundant. |
internal/tags/domain |
No testable logic (Tag struct only has fields, no methods). |
Race Detector
Go's race detector (-race) detects data races: concurrent access to shared memory without synchronization. It's essential for code with goroutines (like VariantQueue and ratelimit.SQLiteStore).
| Command | What it does |
|---|---|
make test |
Basic tests, fast, no race checks |
make test-race |
Tests with race detector (~10x slower but catches concurrency bugs) |
make test-cover |
Coverage report showing % of statements exercised |
make test-all |
Race detector + coverage (most thorough) |
Performance note: The race detector slows execution significantly because it instruments every memory access. For rapid TDD cycles, use make test. Run make test-race or make test-all before commits.
Project Structure
goartesory/
cmd/
web/main.go # Server entry point (wiring, DI, startup)
setup/main.go # CLI: create admin user
internal/
images/ # Images module
domain/ # Image, Blob entities, repository interfaces
application/ # ImageService (CRUD, variants, proxy)
infrastructure/
sqlite/ # SQLite repositories
http/ # HTTP handlers (public + dashboard)
s3/ # BlobStore adapter (bridges storage module)
tags/ # Tags module
domain/ # Tag entity, repository interface
application/ # TagService (CRUD, slug generation)
infrastructure/
sqlite/ # SQLite repository
http/ # HTTP handlers (dashboard CRUD)
users/ # Users module
domain/ # User entity, repository interface
application/ # AuthService (login, user CRUD, bcrypt)
infrastructure/
sqlite/ # SQLite repository
http/ # HTTP handlers (login, logout, user CRUD)
storage/ # Storage module (shared port)
domain/ # BlobStore interface
infrastructure/
s3/ # S3 adapter (AWS SDK v2)
platform/ # Cross-cutting platform layer
config/ # Configuration from env vars
database/ # SQLite connection + pragmas
middleware/ # auth, csrf, session, logging, recovery, security, ratelimit
router/ # Route registrar interface
response/ # Template rendering + error pages
filesystem/ # Embedded template filesystem
templates/ # All HTML templates (embedded at build time)
migrations/ # Database migrations (7 migration pairs)
.env.example # Environment configuration template
Makefile # Build and development commands
Architecture
Hexagonal Monolith
The application follows a modular monolith with hexagonal architecture (ports & adapters):
- Domain layer: entities, value objects, repository interfaces (no external dependencies)
- Application layer: use cases, services (depends only on domain)
- Infrastructure layer: SQLite repos, HTTP handlers, S3 client (implements domain interfaces)
Module Boundaries
Each module defines its own interfaces. Cross-module dependencies are wired at cmd/web/main.go:
images module ──(needs)──▶ BlobStore interface (defined locally)
▲
│ implements
│
storage module ──(provides)──▶ S3 BlobStore adapter
No module accesses another module's internal/ directly.
Database Schema
Tables:
sessions— session storage (SCS/sqlite3store)users— authentication (email, password_hash, role)blobs— file metadata (ActiveStorage-style: key, filename, content_type, byte_size, checksum, metadata)images— image entity (title, description, blob_id FK)tags— tag labels (name, slug)image_tags— many-to-many relationshiprate_limits— rate limiting counters
Routes
Public (no authentication)
| Method | Path | Description |
|---|---|---|
| GET | / |
Gallery homepage (masonry grid) |
| GET | /images/{id} |
Image detail page |
| GET | /blobs/{key}/{variant} |
Image proxy from S3 |
| GET | /dashboard/login |
Login form |
| POST | /dashboard/login |
Process login |
Dashboard (authenticated)
| Method | Path | Access |
|---|---|---|
| GET | /dashboard |
Any user |
| GET | /dashboard/images |
Any user |
| GET | /dashboard/images/new |
Admin |
| POST | /dashboard/images |
Admin |
| GET | /dashboard/images/{id} |
Any user |
| GET | /dashboard/images/{id}/edit |
Admin |
| POST | /dashboard/images/{id}/edit |
Admin |
| POST | /dashboard/images/{id}/delete |
Admin |
| GET | /dashboard/tags |
Any user |
| POST | /dashboard/tags |
Admin |
| GET | /dashboard/tags/{id}/edit |
Admin |
| POST | /dashboard/tags/{id}/edit |
Admin |
| POST | /dashboard/tags/{id}/delete |
Admin |
| GET | /dashboard/users |
Admin |
| POST | /dashboard/users |
Admin |
| GET | /dashboard/users/{id}/edit |
Admin |
| POST | /dashboard/users/{id}/edit |
Admin |
| POST | /dashboard/users/{id}/delete |
Admin |
| POST | /dashboard/logout |
Any user |
Health
| Method | Path | Description |
|---|---|---|
| GET | /health/live |
Liveness probe |
| GET | /health/ready |
Readiness probe (checks DB) |
Security
- Password hashing: bcrypt with cost 12 (
golang.org/x/crypto/bcrypt) - Session management: SCS with SQLite backend, HttpOnly + SameSite=Lax cookies
- CSRF protection: nosurf on all state-changing requests
- Rate limiting: configurable per-route, SQLite backend (login: 10 req/min per IP)
- Security headers: X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy
- X-Forwarded-For support: real IP extraction for reverse proxy setups
- Secrets: all sensitive values via environment variables, never committed
Tech Stack
| Component | Technology |
|---|---|
| Language | Go 1.26 |
| Database | SQLite (modernc.org/sqlite - pure Go, no CGO) |
| Migrations | golang-migrate (CLI tool) |
| HTTP Router | Go 1.22+ http.ServeMux (pattern matching) |
| Sessions | alexedwards/scs + sqlite3store |
| CSRF | justinas/nosurf |
| Middleware | justinas/alice |
| Forms | go-playground/form |
| Image Processing | h2non/bimg (libvips - CGO required) |
| S3 Storage | AWS SDK Go v2 |
| Password Hashing | golang.org/x/crypto/bcrypt |
| Frontend | Bootstrap 5.3.8 + Bootstrap Icons (CDN) |
| UUID | google/uuid |
Deployment
Single Binary
The app compiles to a single static binary (with CGO for libvips). Deploy alongside:
- The binary (
goartesory) - A
.envfile with production configuration - An SQLite database file in a persistent volume
- Run migrations before starting:
make migrate-up
Proxy Setup
The app supports reverse proxies. Set ENV=production to enable:
- Secure cookies (Secure + HttpOnly)
- HSTS headers (via
Strict-Transport-Security)
S3 Storage
Works with any S3-compatible service:
- AWS S3: set
S3_ENDPOINT=https://s3.amazonaws.com,S3_USE_PATH_STYLE=false - MinIO: set
S3_ENDPOINT=http://localhost:9000,S3_USE_PATH_STYLE=true - Cloudflare R2: set
S3_ENDPOINT=https://<account>.r2.cloudflarestorage.com - Backblaze B2: set
S3_ENDPOINT=https://s3.us-west-004.backblazeb2.com
CDN
Set CDN_BASE_URL=https://cdn.example.com to rewrite all image URLs to use your CDN. When empty, images are served through the Go app proxy from S3.
Production Deployment with Docker
The project provides a pre-built Docker image available in the Forgejo Container Registry at git.jesusmarin.dev/angel/goartesory. The image is automatically built and pushed by Forgejo Actions on every push to the master branch.
How It Works
- The CI workflow (
.forgejo/workflows/ci.yml) runs tests and linting on every push and pull request. - On push to
master, it builds a multi-stage Docker image and pushes it to the registry with two tags:git.jesusmarin.dev/angel/goartesory:latestgit.jesusmarin.dev/angel/goartesory:sha-<commit-sha>
- The
docker-compose.ymlfile uses the pre-built image from the registry (no local build needed).
Deploy
# 1. Copy and configure environment
cp .env.example .env
# Edit .env with your production values
# 2. Start the service
docker compose up -d
# Or with Podman:
podman compose up -d
The app will be available at http://<host>:8080.
Docker Image Details
- Base image: Debian Bookworm Slim (minimal runtime)
- Runtime dependencies:
libvips42,libgomp1,ca-certificates - Includes: the Go binary, golang-migrate, templates, and migrations
- Entrypoint: runs database migrations automatically, then starts the server
- Data persistence: SQLite database stored in a Docker volume mounted at
/data
Health Endpoints
The container exposes two health check endpoints:
| Endpoint | Purpose |
|---|---|
GET /health/live |
Liveness probe (always returns 200) |
GET /health/ready |
Readiness probe (checks database connectivity) |
Local Docker Build
For testing the Docker image locally:
make docker-build
This builds an image tagged goartesory:local using the same Dockerfile.
Repository
This project is hosted on Forgejo at git.jesusmarin.dev/angel/goartesory. The Go module path is goartesory.jesusmarin.dev.