Skip to content

Development Guide

This document is based on the following source files:

  • AGENTS.md -- Project entry information: common commands, database standards, coding conventions, API documentation standards, Git conventions, build and deployment
  • Makefile -- All Make targets for build, test, code generation, Docker, etc.
  • Dockerfile -- Multi-stage Docker build flow and runtime environment
  • sqlc.yaml -- sqlc code generation configuration (engine, input/output paths, generation options)
  • go.mod -- Go module declaration, version requirements, and dependency list

Table of Contents

  1. Development Environment Setup
  2. Common Commands
  3. Coding Conventions
  4. Database Operation Standards
  5. API Documentation Standards
  6. Testing Strategy
  7. Build Variants
  8. Git Commit Conventions

1. Development Environment Setup

1.1 Backend Dependencies

ToolVersion RequirementDescription
Go1.26The minimum version declared in go.mod
SQLiteAutomatically embedded at runtimeUses modernc.org/sqlite (pure Go implementation), no system C library installation required
ffmpeg / ffprobeAny recent versionAudio transcoding and metadata probing
sqlclatestSQL code generation: go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
swaglatestSwagger documentation generation (make swagger installs it automatically)
UPXOptionalAutomatically compresses the binary during production builds; skipped if not installed

1.2 Frontend Dependencies

ToolVersion RequirementDescription
Flutter3.29+Cross-platform UI framework
Dart3.7+Bundled with Flutter

The frontend code is located in songloft-player/ (a separate repository); you need to clone it or initialize the submodule first before development.

1.3 Quick Verification

bash
make deps      # Download Go dependencies
make version   # Check Go version
make run       # Start dev mode (admin/admin, port 58091)
               # Visit http://localhost:58091/swagger/index.html to verify

Section sources


2. Common Commands

2.1 Backend Commands

CommandPurpose
make runStart dev mode (with Swagger + pprof, default admin/admin/58091)
make buildBuild the dev version (full, with frontend embedded, -tags dev)
make build-liteBuild the dev version (lite, without frontend embedded, -tags "dev,lite")
make build-prodBuild the production version (full, with frontend embedded)
make build-prod-liteBuild the production version (lite, without frontend embedded)
make testRun all tests
make test-shortQuick tests (skip integration tests)
make test-unitOnly unit tests under internal/
make checkOne-click check: fmt + vet + test
make sqlcRegenerate sqlc code (must run after changing queries/*.sql)
make swaggerRegenerate the Swagger documentation
make bump TYPE=patchBump the version number and tag it

2.2 Frontend Build Commands

Frontend build artifacts are uniformly output to songloft-player-build/ (not songloft-player/build/).

CommandPurpose
make build-frontend-web-embeddedEmbedded mode Web (hides the API address UI)
make build-frontend-webStandalone deployment Web
make build-frontend-{linux,windows,macos,android,ios}Clients for each platform
make build-frontend-allAll platforms supported by the current system

2.3 Frontend Development

bash
cd songloft-player && flutter run -d chrome                                   # standalone
cd songloft-player && flutter run -d chrome --dart-define=DEPLOY_MODE=embedded # embedded

Section sources


3. Coding Conventions

3.1 Project Structure

The backend follows the standard Go layout, with internal/ preventing direct imports by external packages:

internal/
├── app/          # Application entry: initialization, route registration
├── database/     # Database opening, migration, Repository, UnitOfWork
├── handlers/     # HTTP handler layer
├── middleware/    # Authentication, logging, and other middleware
├── models/       # Data model definitions
├── services/     # Business logic layer
└── ...

3.2 Core Conventions

  • Routing: Chi v5 + JWT dual token, with route registration concentrated in internal/app/routers.go
  • Dependency injection: the service layer only receives Repository interfaces, not the DB instance
  • Logging: the standard library slog, no third-party framework introduced
  • HTTP errors: uniformly use the respondError function
  • API response format: RESTful direct return, no {code, data, message} envelope; errors uniformly {"error": "...", "detail": "..."}

3.3 Data Access Layer (No ORM)

ScenarioToolDescription
Fixed SQLsqlcWritten in database/queries/*.sql, make sqlc generates the Go code
Dynamic SQL (variable-length WHERE/SET)squirrelUsed in *_repository.go; string concatenation is prohibited
Cross-table writes (transactions)RunInTx + UnitOfWorkOperate on multiple Repositories under the same *sql.Tx

Section sources


4. Database Operation Standards

The content of this section constitutes a project iron law that all developers must strictly follow. The complete access stack: goose migration → sqlc fixed SQL → squirrel dynamic SQL → Repository → UnitOfWork.

4.1 Schema Changes (Migrations)

  • Migration files are placed in internal/database/migrations/000N_xxx.sql
  • goose.Up runs automatically at startup, no manual operation needed
  • Prohibited to manually ALTER data/songloft.db directly

4.2 Fixed SQL (sqlc)

  • Write SQL in internal/database/queries/{table}.sql, then run make sqlc after modifying
  • Generated artifacts are in internal/database/sqlc/ and must be committed

Key sqlc configuration (sqlc.yaml) options: engine: sqlite, schema pointing to the migrations directory, emit_interface: true to generate interfaces for easier testing, emit_empty_slices: true to return [] instead of nil for empty results.

4.3 Dynamic SQL and Transactions

  • Variable-length WHERE/SET are built with squirrel in *_repository.go; string concatenation is prohibited
  • Cross-table writes must use db.RunInTx(ctx, func(ctx, uow)); UnitOfWork provides all Repositories under the same transaction
  • Prohibited for the service layer to manually BeginTx, otherwise SQLite will SQLITE_BUSY deadlock

4.4 Error Semantics and Built-in Data

  • The Repository uniformly returns database.ErrNotFound on a miss; the service uses errors.Is to distinguish it
  • Migrations preset the playlists id=1 "Favorites", id=2 "Radio Favorites", along with the music_path/jwt_secret/source_* default config -- remember to subtract these when asserting row counts in tests

Section sources


5. API Documentation Standards

The content of this section constitutes a project iron law. API documentation is generated by swaggo/swag from code comments and is the single source of truth for frontend development and external integration.

5.1 Core Principle

Any handler registered in internal/app/routers.go (including its sub-registration functions) must have swag comments. No exemptions. Even dynamic routes, catch-alls, and reverse-proxy endpoints must have them written -- it suffices to explain in the @Description.

5.2 Required Fields (at least 7 per handler)

go
// @Summary <one-line summary>
// @Description <detailed description; state side effects/default values/error code trigger conditions>
// @Tags <business group>
// @Produce json
// @Success 200 {object} <return type> "<description>"
// @Security BearerAuth
// @Router /<path> [<method>]

Additional fields: for endpoints with a request body, add @Accept json + @Param ... body; for endpoints with an error path, add @Failure; public endpoints omit @Security.

5.3 Business Tag List

Reuse existing tags; do not casually create new tags: Song Management, Playlist Management, Radio and HLS, Scan Management, Config Management, Cache Management, JS Plugin, Data Backup, Settings, Upgrade, Authentication.

5.4 Multiple Aliases and Catch-all Routes

  • Multiple alias paths → one @Router line each
  • r.HandleFunc(...) catch-all → list all actual methods, one @Router line each
  • Dynamic paths → note in the @Description that "it is a dynamic route, OpenAPI serves only as a placeholder"

5.5 Post-Modification Verification Flow

bash
make swagger                                  # Generate the documentation
grep '<your-new-path>' docs/swagger.json      # Confirm the new path exists
make run                                       # Visually inspect in the Swagger UI

The generated docs/swagger.json, docs/swagger.yaml, and docs/docs.go must be committed.

Section sources


6. Testing Strategy

6.1 Core Principles

  • Test files *_test.go are in the same directory as the source code under test
  • Prohibited to hand-write a mock DB -- use testutil.OpenMemoryDB(t) to run a real :memory: SQLite + real Repository
go
func TestSongService(t *testing.T) {
    db := testutil.OpenMemoryDB(t)
    repo := database.NewSongRepository(db)
    svc := services.NewSongService(repo)
    // ... test real behavior
}

Advantages: covers the real SQL path, in-memory DB starts extremely fast, and automatically runs goose migrations to guarantee schema consistency.

6.2 Notes

  • Migrations preset built-in playlists and default config; must be subtracted when asserting row counts
  • The dependency injection design ensures the service can be tested independently, without needing to (and without a reason to) mock the database layer

6.3 Test Commands

bash
make test          # All tests
make test-short    # Skip integration tests
make test-unit     # Only internal/
make test-coverage # Coverage report
make bench         # Benchmark tests

Section sources


7. Build Variants

7.1 Two Orthogonal Dimensions

DimensionValuesMeaning
VERSIONdev / X.Y.Zdev automatically enables -tags dev (Swagger + pprof)
BUILD_TYPElite / empty (full)lite does not embed the frontend resources

Prohibited to mix them (such as BUILD_TYPE=dev).

7.2 Build Matrix

VERSIONBUILD_TYPEMake Targetbuild tags
devfullmake builddev
devlitemake build-litedev,lite
X.Y.Zfullmake build-prodnone
X.Y.Zlitemake build-prod-litelite

7.3 CGO and Cross-Compilation

The Makefile globally sets CGO_ENABLED=0, SQLite uses the pure Go implementation (modernc.org/sqlite), and all-platform cross-compilation is supported:

bash
make build-cross GOOS=linux GOARCH=amd64 OUTPUT=songloft-linux-amd64
make build-cross GOOS=linux GOARCH=arm64 OUTPUT=songloft-linux-arm64
make build-cross GOOS=windows GOARCH=amd64 OUTPUT=songloft.exe

Production builds automatically detect UPX and compress. At compile time, Version, GitCommit, BuildTime, and BuildType are injected into the internal/version package via -ldflags.

7.4 Docker Build

Multi-stage build: golang:1.26-alpine (build stage, accelerated by BuildKit cache mounts) → alpine:latest (runtime, with ffmpeg, ALSA, ca-certificates).

bash
make docker-build                                               # Test image
docker build --build-arg VERSION=2.10.0 -t songloft .           # Full version
docker build --build-arg LITE_BUILD=true -t songloft:lite .     # Lite version

7.5 Frontend Embedding Path

When building the full version, the frontend artifact must be located at songloft-player-build/web-embedded/ (not songloft-player/build/), embedded by internal/app/embed.go.

Section sources


8. Git Commit Conventions

8.1 Commit Format

Follows Conventional Commits: type(scope): description

Common types: feat (new feature), fix (bug fix), refactor (refactoring), docs (documentation), test (testing), chore (build/config), perf (performance optimization).

feat(cache): add custom cache directory support
fix(fingerprint): remove invalid length threshold in ExtractFingerprint

8.2 Prohibitions

  • Commit messages must not add a Co-Authored-By trailer

8.3 Submodules Referencing Parent Repository Issues

When a submodule commit references a parent repository issue, the full path must be used:

fix(player): handle CORS error, see songloft-org/songloft#155   # Correct
fix(player): handle CORS error, see #155                         # Wrong (resolves to a submodule issue)

8.4 Version Release

bash
make bump              # patch (2.10.0 → 2.10.1)
make bump TYPE=minor   # minor (2.10.0 → 2.11.0)
make bump TYPE=major   # major (2.10.0 → 3.0.0)

After pushing the tag, .github/workflows/release.yml automatically builds and releases.

Section sources