soro

package module
v0.0.0-...-d364ca2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 17, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

README

Soro

Soro is an opinionated Go application framework for building production REST APIs quickly, combining convention-driven development with idiomatic Go.

Soro is developed by DataSoro. Its developer experience is inspired by the productivity of Rails API applications, while its implementation keeps normal Go structs, interfaces, generics, context.Context, explicit dependencies, PostgreSQL, and Bun available to application code.

Status: pre-release. Phases 1 through 5 are implemented; the public API is not stable.

Implemented foundation

The current foundation includes:

  • a typed application container and strict layered configuration;
  • one shared pgx pool bridged to Bun and River;
  • UUIDv7 IDs, UTC timestamps, actor fields, and JSONB metadata;
  • typed generic repositories and Bun escape hatches;
  • transactional create, update, soft delete, restore, and force delete;
  • joined nested transactions with outermost AfterCommit/AfterRollback behavior;
  • all required optional model hooks plus deterministic global and registered hooks;
  • persisted-state dirty tracking;
  • contextual and declarative validation with normalized errors;
  • readable PostgreSQL migrations and partial unique indexes;
  • PostgreSQL integration tests and a compiling example.
  • Huma-backed versioned routing, OpenAPI 3.1, and API documentation;
  • typed serializers and generic REST resources with explicit input mappers;
  • pagination, allowlisted filtering, literal ILIKE search, and sorting;
  • standard error envelopes, server-generated request IDs, and safe panic recovery;
  • resource authorization/callback/scope hooks and route introspection.
  • River-backed typed jobs sharing Bun's pool and transaction;
  • SMTP, console, and capture mail with transaction-safe SendLater;
  • structured HTTP/job/mail logging and W3C trace propagation;
  • OpenTelemetry tracing, OTLP HTTP export, and Prometheus metrics;
  • /health, /ready, and /metrics infrastructure endpoints;
  • configured HTTP timeouts and graceful server/worker shutdown.
  • the Cobra-based soro CLI with runtime, database, job, and OpenAPI commands;
  • conflict-safe application, model, resource, migration, serializer, validator, job, and mailer generators;
  • runtime-discovered PostgreSQL SQL migrations with explicit Up/Down sections;
  • generated application and PostgreSQL migration acceptance tests.
  • schema-isolated test applications, typed factories, HTTP helpers, captured mail assertions, and synchronous job-handler tests.

Phase 5 includes schema-isolated test applications, typed factories, development logging and secret redaction, generator customization, benchmark baselines, expanded examples, and the pre-v1 compatibility/release policy.

CLI quick start

Build the pre-release CLI from this checkout:

mise exec -- go install ./cmd/soro

Until Soro has a tagged module release, point a generated application at this checkout:

soro new customer-api --module example.com/customer-api --soro-replace /path/to/soro
cd customer-api
soro generate resource User \
  email:string:unique:index \
  first_name:string \
  last_name:string \
  active:bool:default=true
soro db create
soro db migrate
soro server

The resource generator writes a model, migration, serializer, input validators, CRUD resource, registration, and tests. Generated SQL uses UUID primary keys, JSONB metadata, TIMESTAMPTZ, soft deletion, and partial unique indexes. See CLI and generators.

Requirements

  • Go 1.26+
  • PostgreSQL 17+ for integration tests and the example

The repository pins Go 1.26.6 through mise.toml.

Model and repository

type User struct {
	model.Base
	Email  string `bun:"email,notnull" validate:"required,email"`
	Active bool   `bun:"active,notnull,default:true"`
}

func (u *User) BeforeCreate(ctx context.Context, lc *lifecycle.Context) error {
	u.Email = strings.ToLower(strings.TrimSpace(u.Email))
	return nil
}

func (u *User) AfterUpdate(ctx context.Context, lc *lifecycle.Context) error {
	if lc.Changes.Changed("Email") {
		oldEmail, newEmail, _ := lc.Changes.Values("Email")
		_ = oldEmail
		_ = newEmail
	}
	return nil
}
users := repository.New[User](app.DB)
user := &User{
	Base:  model.Base{Name: "Dustin"},
	Email: "USER@EXAMPLE.COM",
}

if err := users.Create(ctx, user); err != nil { /* handle */ }
found, err := users.Find(ctx, user.ID)
if err != nil { /* handle */ }
found.Active = true
if err := users.Update(ctx, found); err != nil { /* handle */ }
if err := users.Delete(ctx, found.ID); err != nil { /* soft delete */ }

deleted, err := users.OnlyDeleted().Find(ctx, found.ID)
if err != nil { /* handle */ }
if err := users.Restore(ctx, deleted.ID); err != nil { /* handle */ }
if err := users.ForceDelete(ctx, deleted.ID); err != nil { /* explicit physical delete */ }

Normal reads exclude deleted rows. WithDeleted() includes both states, and OnlyDeleted() returns deleted rows. Scope methods return repository copies and do not mutate shared state.

HTTP resources

Application input, model, and response types remain separate. The compiling basic example configures a user resource with explicit mapping and serialization:

users, err := basic.NewUserResource(repository.New[basic.User](app.DB))
if err != nil { /* handle */ }

err = app.API.Version("v1", func(v1 *api.Router) {
	if err := v1.Resource("/users", users); err != nil { /* handle */ }
})

This registers:

GET    /api/v1/users
GET    /api/v1/users/{id}
POST   /api/v1/users
PATCH  /api/v1/users/{id}
DELETE /api/v1/users/{id}

DELETE is a soft delete. OpenAPI is served at /openapi.json and /openapi.yaml, with interactive documentation at /docs. List resources accept page, per_page, search, allowlisted filter[...] parameters, and sort fields configured by the resource.

Transactions

Repository methods join a Soro transaction carried by the callback context:

err := users.Transaction(ctx, func(txCtx context.Context, txUsers *repository.Repository[User]) error {
	if err := txUsers.Create(txCtx, user); err != nil {
		return err
	}
	return txUsers.Create(txCtx, anotherUser)
})

Nested calls join the outer SQL transaction. A nested error marks the outer transaction rollback-only, even if an intermediate callback catches it. Phase 1 does not implement savepoints. AfterCommit executes only after the outer commit succeeds; an error from it is returned after data has committed and cannot roll the transaction back.

Jobs and mail

Job arguments use a stable kind and ordinary JSON fields:

type SendWelcomeEmail struct {
	UserID uuid.UUID `json:"user_id" river:"unique"`
}

func (SendWelcomeEmail) Kind() string { return "send_welcome_email" }

err := jobs.Register(app.Jobs, func(ctx context.Context, args SendWelcomeEmail) error {
	return sendWelcome(ctx, args.UserID)
})

Enqueue normally or inside the current Soro transaction:

_, err := app.Jobs.Enqueue(ctx, SendWelcomeEmail{UserID: user.ID},
	jobs.Queue("mailers"), jobs.Priority(2), jobs.UniqueByArgs())

When ctx carries a Soro transaction, Enqueue automatically uses River's transactional insertion. EnqueueTx is available when transactional context must be required explicitly.

Mail delivery is immediate or queued:

delivery := app.Mailer.Delivery(&mail.Message{
	To: []string{user.Email}, Subject: "Welcome", Text: "Hello",
})
err = delivery.Send(ctx)
_, err = delivery.SendLater(ctx, jobs.Delay(5*time.Minute))

Configuration

Configuration precedence is:

framework defaults
config/application.yaml
config/{SORO_ENV}.yaml
environment variables

Supported variables include SORO_ENV, SORO_APP_NAME, SORO_APP_VERSION, DATABASE_URL, SORO_LOG_LEVEL, SORO_LOG_FORMAT, HTTP timeout variables, SORO_JOBS_*, SORO_MAIL_*, SMTP_*, SORO_OTEL_ENABLED, OTEL_EXPORTER_OTLP_ENDPOINT, and the database pool variables. Unknown YAML fields fail startup. Production requires DATABASE_URL and SMTP mail configuration. The default logger uses readable text in development, JSON in production, and redacts standard secret-bearing fields. See configuration.

Run the example

Start PostgreSQL using any local installation or the checked-in Compose service:

docker compose up -d postgres

In another shell:

export DATABASE_URL='postgres://postgres:postgres@localhost:5432/soro?sslmode=disable'
mise exec -- go run ./examples/basic/cmd/demo

The persistence demonstration applies its migrations, idempotently seeds an Account/User/Project relationship graph, creates and updates a user, soft-deletes it, restores it, and explicitly force-deletes it. Run the HTTP example instead with:

mise exec -- go run ./examples/basic/cmd/server

Then open http://localhost:8080/docs or call the /api/v1/accounts, /api/v1/users, and /api/v1/projects resources. The example demonstrates explicit UUID relationships, metadata, factories/seeds, lifecycle changes, allowlisted filtering/search/sorting, transactional jobs, captured or SMTP mail, tracing, and metrics.

Set SORO_JOBS_ENABLED=true to work the example's transactionally enqueued welcome-mail jobs in the server process. Generated applications can run a dedicated worker with soro jobs work.

Tests

Unit tests do not require external services. PostgreSQL integration tests use schema isolation and run when SORO_TEST_DATABASE_URL is present:

mise exec -- go test ./...

SORO_TEST_DATABASE_URL='postgres://postgres:postgres@localhost:5432/soro_test?sslmode=disable' \
  mise exec -- go test ./...

SORO_TEST_DATABASE_URL='postgres://postgres:postgres@localhost:5432/soro_test?sslmode=disable' \
  mise exec -- go test -race ./...

CI always supplies PostgreSQL, so integration tests cannot silently skip there. CI also generates an aggregate coverage profile and enforces a 70% statement floor.

Design documents

License

Apache License 2.0. See LICENSE.

Documentation

Overview

Package soro provides the application container for the Soro framework.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type App

type App struct {
	Config        *config.Config
	DB            *database.DB
	API           *api.API
	Logger        *slog.Logger
	Jobs          *jobs.Client
	Mailer        *mail.Client
	Observability *observability.Provider
	Health        *health.Registry
	// contains filtered or unexported fields
}

func New

func New(ctx context.Context, options ...Option) (*App, error)

func (*App) Close

func (app *App) Close() error

func (*App) Serve

func (app *App) Serve(ctx context.Context) error

type Option

type Option func(*appSettings)

func WithAPI

func WithAPI(httpAPI *api.API) Option

WithAPI replaces the default HTTP API, primarily for tests and advanced setup.

func WithAudienceAuthorizer

func WithAudienceAuthorizer(authorizer api.AudienceAuthorizer) Option

WithAudienceAuthorizer connects endpoint audience policies to the application's principal scopes and software-client authentication.

func WithConfig

func WithConfig(settings *config.Config) Option

func WithDatabase

func WithDatabase(db *database.DB) Option

WithDatabase replaces database initialization. The App owns and closes db.

func WithHealth

func WithHealth(registry *health.Registry) Option

func WithJobs

func WithJobs(client *jobs.Client) Option

func WithLogger

func WithLogger(logger *slog.Logger) Option

func WithMailTransport

func WithMailTransport(transport mail.Transport) Option

func WithMailer

func WithMailer(client *mail.Client) Option

func WithObservability

func WithObservability(provider *observability.Provider) Option

Directories

Path Synopsis
Package api wraps Huma with Soro routing, errors, resources, and middleware.
Package api wraps Huma with Soro routing, errors, resources, and middleware.
Package auth defines Soro's authentication-neutral request principal context.
Package auth defines Soro's authentication-neutral request principal context.
Package cli implements the Soro command-line interface.
Package cli implements the Soro command-line interface.
cmd
soro command
Package config loads and validates Soro's typed application configuration.
Package config loads and validates Soro's typed application configuration.
Package database owns Soro's PostgreSQL pool, Bun bridge, and transactions.
Package database owns Soro's PostgreSQL pool, Bun bridge, and transactions.
Package errors defines stable errors shared by Soro's non-HTTP layers.
Package errors defines stable errors shared by Soro's non-HTTP layers.
examples
basic
Package basic is a compiling Phase 1 example application model.
Package basic is a compiling Phase 1 example application model.
basic/cmd/demo command
Package factory provides typed, persistence-agnostic test data factories.
Package factory provides typed, persistence-agnostic test data factories.
Package generate implements Soro's deterministic application and component generators.
Package generate implements Soro's deterministic application and component generators.
Package health provides cheap liveness and dependency readiness endpoints.
Package health provides cheap liveness and dependency readiness endpoints.
internal
testdb
Package testdb provides schema-isolated PostgreSQL databases to Soro tests.
Package testdb provides schema-isolated PostgreSQL databases to Soro tests.
Package jobs wraps River with Soro transactions, options, and telemetry.
Package jobs wraps River with Soro transactions, options, and telemetry.
Package lifecycle defines Soro's model operation hooks and change context.
Package lifecycle defines Soro's model operation hooks and change context.
Package mail provides Soro messages, templates, transports, and queued delivery.
Package mail provides Soro messages, templates, transports, and queued delivery.
Package migrate applies readable PostgreSQL migrations through Soro transactions.
Package migrate applies readable PostgreSQL migrations through Soro transactions.
Package model provides Soro's persistence model primitives.
Package model provides Soro's persistence model primitives.
Package observability owns Soro tracing, metrics, and HTTP instrumentation.
Package observability owns Soro tracing, metrics, and HTTP instrumentation.
Package query parses and applies safe, resource-defined PostgreSQL queries.
Package query parses and applies safe, resource-defined PostgreSQL queries.
Package repository provides typed PostgreSQL persistence for Soro models.
Package repository provides typed PostgreSQL persistence for Soro models.
Package serializer keeps persistence models separate from public responses.
Package serializer keeps persistence models separate from public responses.
Package sorotest provides schema-isolated Soro application test helpers.
Package sorotest provides schema-isolated Soro application test helpers.
Package validation provides Soro's HTTP-independent validation engine.
Package validation provides Soro's HTTP-independent validation engine.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL