This is the full developer documentation for Platforma
# platforma
> Golang backend application framework
### Install package
[Section titled “Install package”](#install-package)
to use framework in your application
```bash
go get -u github.com/platforma-dev/platforma@latest
```
### Install command
[Section titled “Install command”](#install-command)
to use `platforma` CLI
```bash
go install github.com/platforma-dev/platforma@latest
```
# CLI
## docs
[Section titled “docs”](#docs)
Serving this documentations
```bash
platforma docs
```
Parameters:
* `-p`, `--port` - set port on what documentation is served. Default is `4444`
## generate
[Section titled “generate”](#generate)
### domain
[Section titled “domain”](#domain)
```bash
platforma generate domain
```
# Getting started
> Installation and first application
`platforma` is a framework for building backend applications with Golang. It’s has several packages like `httpserver` and `database` that provide solid foundation which you can build on top of.
## Installation
[Section titled “Installation”](#installation)
```shell
go get -u github.com/platforma-dev/platforma@latest
```
## First application
[Section titled “First application”](#first-application)
main.go
```go
package main
import (
"context"
"fmt"
"time"
"github.com/platforma-dev/platforma/application"
"github.com/platforma-dev/platforma/log"
)
type Clock struct{}
func (r *Clock) Run(ctx context.Context) error {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
log.InfoContext(ctx, "tick")
case <-ctx.Done():
log.InfoContext(ctx, "finished")
return fmt.Errorf("context error: %w", ctx.Err())
}
}
}
func main() {
ctx := context.Background()
app := application.New()
app.RegisterService("clock", &Clock{})
if err := app.Run(ctx); err != nil {
log.ErrorContext(ctx, "app finished with error", "error", err)
}
}
```
# application
The `application` package provides the central orchestrator for managing the lifecycle of your application, including startup tasks, services, databases, and health checks.
Core Components:
* `Application`: Central orchestrator that manages startup tasks, services, databases, and health checks
* `Runner`: Interface that services and startup tasks must implement to be executed by the application
* `RunnerFunc`: Function type that implements `Runner` for simple inline tasks
* `StartupTaskConfig`: Configuration for startup tasks with name and abort-on-error behavior
* `Domain`: Interface for self-contained modules that bundle repository and other components
* `Healthchecker`: Interface for services that can report their health status
* `HealthCheckHandler`: HTTP handler for exposing application health as JSON
* `ApplicationHealth`: Tracks overall application health and individual service statuses
* `ServiceHealth`: Health status for a single service including start time and errors
[Full package docs at pkg.go.dev](https://pkg.go.dev/github.com/platforma-dev/platforma/application)
## Step-by-step guide
[Section titled “Step-by-step guide”](#step-by-step-guide)
1. Create a new `Application` instance
```go
app := application.New()
```
2. Define a service that implements the `Runner` interface
```go
type MyService struct{}
func (s *MyService) Run(ctx context.Context) error {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
log.InfoContext(ctx, "service tick")
case <-ctx.Done():
log.InfoContext(ctx, "service shutting down")
return nil
}
}
}
```
The `Run` method should block until the context is canceled or an error occurs.
3. Register startup tasks that run before services start
```go
app.OnStartFunc(func(ctx context.Context) error {
log.InfoContext(ctx, "initialization complete")
return nil
}, application.StartupTaskConfig{
Name: "init",
AbortOnError: true,
})
```
Setting `AbortOnError` to `true` stops the application if this task fails. Use `OnStart` for `Runner` implementations or `OnStartFunc` for inline functions.
4. Register your service with the application
```go
app.RegisterService("my-service", &MyService{})
```
5. Run the application
```go
ctx := context.Background()
if err := app.Run(ctx); err != nil {
log.ErrorContext(ctx, "app finished with error", "error", err)
}
```
The `Run` method parses CLI arguments from `os.Args`:
```bash
# Run database migrations
./myapp migrate
# Start the application (services and startup tasks)
./myapp run
# Show usage
./myapp --help
```
6. Run migrations first, then start your app
```bash
./myapp migrate
./myapp run
```
You will see output like this:
```plaintext
time=2025-11-11T22:01:26.630+03:00 level=INFO msg="starting application" startupTasks=1
time=2025-11-11T22:01:26.630+03:00 level=INFO msg="running task" task=init index=0
time=2025-11-11T22:01:26.630+03:00 level=INFO msg="initialization complete"
time=2025-11-11T22:01:26.631+03:00 level=INFO msg="starting service" serviceName=my-service
time=2025-11-11T22:01:27.631+03:00 level=INFO msg="service tick"
time=2025-11-11T22:01:28.631+03:00 level=INFO msg="service tick"
```
Press Ctrl+C to gracefully shutdown the application.
## CLI commands
[Section titled “CLI commands”](#cli-commands)
The `Run` method reads `os.Args` and executes the appropriate command:
| Command | Description |
| -------------- | ------------------------------------------------ |
| `run` | Start the application (startup tasks + services) |
| `migrate` | Run database migrations and exit |
| `--help`, `-h` | Show usage information |
If no command is provided, usage information is printed.
## Execution order
[Section titled “Execution order”](#execution-order)
When you run `./myapp run`, the following happens in order:
1. **Startup tasks** - Tasks run sequentially in registration order
2. **Services** - All services start concurrently in separate goroutines
3. **Wait** - Application waits for context cancellation (Ctrl+C)
4. **Shutdown** - Services receive context cancellation for graceful shutdown
When you run `./myapp migrate`:
1. **Database migrations** - All registered databases run their migrations
2. **Exit** - Application exits after migrations complete
## Register methods
[Section titled “Register methods”](#register-methods)
### RegisterService
[Section titled “RegisterService”](#registerservice)
Registers a named service that runs concurrently when the application starts.
```go
app.RegisterService("api", httpServer)
app.RegisterService("queue-processor", processor)
```
If the service implements `Healthchecker`, its health status is automatically tracked.
### RegisterDatabase
[Section titled “RegisterDatabase”](#registerdatabase)
Registers a database connection. Migrations are run when you execute the `migrate` command.
```go
db, _ := database.New("postgres://user:pass@localhost:5432/mydb?sslmode=disable")
app.RegisterDatabase("main", db)
```
### RegisterRepository
[Section titled “RegisterRepository”](#registerrepository)
Registers a repository with a database. The repository must have a `Migrations()` method.
```go
app.RegisterRepository("main", "users", userRepo)
```
### RegisterDomain
[Section titled “RegisterDomain”](#registerdomain)
Registers a domain module. If a database name is provided, the domain’s repository is registered automatically.
```go
app.RegisterDomain("auth", "main", authDomain)
```
## Health checks
[Section titled “Health checks”](#health-checks)
Services implementing `Healthchecker` have their health tracked automatically:
```go
type Healthchecker interface {
Healthcheck(context.Context) any
}
```
Expose health via HTTP using `HealthCheckHandler`:
```go
api.Handle("/health", application.NewHealthCheckHandler(app))
```
The response includes application start time and per-service status:
```json
{
"startedAt": "2025-01-01T12:00:00Z",
"services": {
"api": {
"status": "STARTED",
"startedAt": "2025-01-01T12:00:00Z"
}
}
}
```
## Error handling
[Section titled “Error handling”](#error-handling)
The application returns specific error types:
* `ErrUnknownCommand` - Returned when an unknown CLI command is provided
* `ErrStartupTaskFailed` - Returned when a startup task with `AbortOnError: true` fails
* `ErrDatabaseMigrationFailed` - Returned when database migration fails (from `migrate` command)
Both error types support unwrapping to get the underlying error:
```go
if err := app.Run(ctx); err != nil {
var migrationErr *application.ErrDatabaseMigrationFailed
if errors.As(err, &migrationErr) {
log.ErrorContext(ctx, "migration failed", "cause", migrationErr.Unwrap())
}
}
```
## Complete example
[Section titled “Complete example”](#complete-example)
application.go
```go
package main
import (
"context"
"fmt"
"time"
"github.com/platforma-dev/platforma/application"
"github.com/platforma-dev/platforma/log"
)
type Clock struct{}
func (r *Clock) Run(ctx context.Context) error {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
log.InfoContext(ctx, "tick")
case <-ctx.Done():
log.InfoContext(ctx, "finished")
return fmt.Errorf("context error: %w", ctx.Err())
}
}
}
func main() {
ctx := context.Background()
app := application.New()
app.RegisterService("clock", &Clock{})
if err := app.Run(ctx); err != nil {
log.ErrorContext(ctx, "app finished with error", "error", err)
}
}
```
# auth
The `auth` package provides session-based authentication with username/password credentials, bundled as a self-contained domain.
Core Components:
* `Domain`: Bundles repository, service, HTTP handlers, and middleware together. Implements `application.Domain` interface for registration with an `Application`.
* `Service`: Core authentication logic for user registration, login/logout, password changes, and user deletion.
* `Repository`: PostgreSQL storage for users with automatic schema migrations.
* `User`: User model with ID, username, hashed password, salt, timestamps, and status.
* `AuthenticationMiddleware`: HTTP middleware that validates session cookies and injects the authenticated user into request context.
* `UserCleanupJob`: Job struct for enqueueing post-deletion cleanup tasks.
* `UserFromContext`: Helper function to retrieve the authenticated user from request context.
[Full package docs at pkg.go.dev](https://pkg.go.dev/github.com/platforma-dev/platforma/auth)
## Step-by-step guide
[Section titled “Step-by-step guide”](#step-by-step-guide)
1. Set up the database and session storage
```go
db, err := database.New("postgres://user:pass@localhost:5432/mydb?sslmode=disable")
if err != nil {
log.ErrorContext(ctx, "failed to connect to database", "error", err)
return
}
sessionDomain := session.New(db.Connection())
```
The auth package requires a database connection and session storage. The `session` package provides a compatible session storage implementation.
2. Create the auth domain
```go
authDomain := auth.New(
db.Connection(), // database connection
sessionDomain.Service, // session storage
"session_id", // cookie name for sessions
nil, // username validator (nil uses default)
nil, // password validator (nil uses default)
nil, // cleanup job enqueuer (optional)
)
```
Default validators require usernames to be 5-20 characters and passwords to be 8-100 characters. Pass custom validator functions to override.
3. Register domains with the application
```go
app := application.New()
app.RegisterDatabase("main", db)
app.RegisterDomain("session", "main", sessionDomain)
app.RegisterDomain("auth", "main", authDomain)
```
Registering domains with a database name automatically registers their repositories for migrations.
4. Create an HTTP server and mount the auth endpoints
```go
api := httpserver.New("8080", 3*time.Second)
api.HandleGroup("/auth", authDomain.HandleGroup)
```
This exposes the following endpoints:
* `POST /auth/register` - Create a new user
* `POST /auth/login` - Login and receive session cookie
* `POST /auth/logout` - Clear session
* `GET /auth/me` - Get current user info
* `POST /auth/change-password` - Change password (requires auth)
* `DELETE /auth/me` - Delete user account (requires auth)
5. Protect routes with authentication middleware
```go
protectedGroup := httpserver.NewHandlerGroup()
protectedGroup.Use(authDomain.Middleware)
protectedGroup.HandleFunc("/profile", func(w http.ResponseWriter, r *http.Request) {
user := auth.UserFromContext(r.Context())
if user != nil {
w.Write([]byte("Hello, " + user.Username))
}
})
api.HandleGroup("/api", protectedGroup)
```
The `AuthenticationMiddleware` returns 401 Unauthorized if no valid session is found. Use `auth.UserFromContext()` to access the authenticated user.
6. Register the server and run the application
```go
app.RegisterService("api", api)
if err := app.Run(ctx); err != nil {
log.ErrorContext(ctx, "app finished with error", "error", err)
}
```
Expected output:
```plaintext
time=2025-01-01T12:00:00.000+00:00 level=INFO msg="running migrations" database=main
time=2025-01-01T12:00:00.010+00:00 level=INFO msg="starting service" serviceName=api
time=2025-01-01T12:00:00.010+00:00 level=INFO msg="starting http server" address=:8080
```
## Using with Application
[Section titled “Using with Application”](#using-with-application)
The `auth.Domain` implements the `application.Domain` interface, so it integrates seamlessly with an `Application`:
```go
app := application.New()
// Set up database
db, _ := database.New("postgres://user:pass@localhost:5432/mydb?sslmode=disable")
app.RegisterDatabase("main", db)
// Set up session and auth domains
sessionDomain := session.New(db.Connection())
app.RegisterDomain("session", "main", sessionDomain)
authDomain := auth.New(db.Connection(), sessionDomain.Service, "session_id", nil, nil, nil)
app.RegisterDomain("auth", "main", authDomain)
// Set up HTTP server with auth endpoints
api := httpserver.New("8080", 3*time.Second)
api.HandleGroup("/auth", authDomain.HandleGroup)
app.RegisterService("api", api)
app.Run(ctx)
```
## HTTP endpoints
[Section titled “HTTP endpoints”](#http-endpoints)
| Endpoint | Method | Auth Required | Description |
| ------------------ | ------ | ------------- | ----------------------------------------------------------------------- |
| `/register` | POST | No | Create new user with `{"login": "...", "password": "..."}` |
| `/login` | POST | No | Login with `{"login": "...", "password": "..."}`, sets session cookie |
| `/logout` | POST | No | Clears session cookie |
| `/me` | GET | No | Returns `{"username": "..."}` if authenticated, 401 otherwise |
| `/change-password` | POST | Yes | Change password with `{"currentPassword": "...", "newPassword": "..."}` |
| `/me` | DELETE | Yes | Delete user account and all sessions |
## Custom validators
[Section titled “Custom validators”](#custom-validators)
Override the default username and password validation:
```go
usernameValidator := func(username string) error {
if len(username) < 3 {
return errors.New("username must be at least 3 characters")
}
if !regexp.MustCompile(`^[a-zA-Z0-9_]+$`).MatchString(username) {
return errors.New("username can only contain letters, numbers, and underscores")
}
return nil
}
passwordValidator := func(password string) error {
if len(password) < 12 {
return errors.New("password must be at least 12 characters")
}
return nil
}
authDomain := auth.New(db.Connection(), sessionDomain.Service, "session_id",
usernameValidator, passwordValidator, nil)
```
## User cleanup jobs
[Section titled “User cleanup jobs”](#user-cleanup-jobs)
When a user is deleted, you can enqueue cleanup jobs to handle related data. The `queue.Processor` implements the required interface directly:
```go
// Create a processor for cleanup jobs
cleanupHandler := queue.HandlerFunc[auth.UserCleanupJob](func(ctx context.Context, job auth.UserCleanupJob) {
log.InfoContext(ctx, "cleaning up user data", "user_id", job.UserID, "deleted_at", job.DeletedAt)
// Delete user's files, posts, comments, etc.
})
cleanupQueue := queue.NewChanQueue[auth.UserCleanupJob](100, 5*time.Second)
cleanupProcessor := queue.New(cleanupHandler, cleanupQueue, 2, 10*time.Second)
// Pass processor directly to auth.New()
authDomain := auth.New(db.Connection(), sessionDomain.Service, "session_id",
nil, nil, cleanupProcessor)
// Register processor as a service
app.RegisterService("cleanup", cleanupProcessor)
```
The `UserCleanupJob` contains `UserID` and `DeletedAt` fields for processing cleanup tasks asynchronously.
## Error types
[Section titled “Error types”](#error-types)
The package exports these errors for handling specific cases:
* `ErrUserNotFound` - User does not exist
* `ErrWrongUserOrPassword` - Invalid credentials during login
* `ErrInvalidUsername` / `ErrShortUsername` / `ErrLongUsername` - Username validation failed
* `ErrInvalidPassword` / `ErrShortPassword` / `ErrLongPassword` - Password validation failed
* `ErrCurrentPasswordIncorrect` - Current password wrong during password change
## Complete example
[Section titled “Complete example”](#complete-example)
auth.go
```go
package main
import (
"context"
"net/http"
"time"
"github.com/platforma-dev/platforma/application"
"github.com/platforma-dev/platforma/auth"
"github.com/platforma-dev/platforma/database"
"github.com/platforma-dev/platforma/httpserver"
"github.com/platforma-dev/platforma/log"
"github.com/platforma-dev/platforma/session"
)
func main() {
ctx := context.Background()
db, err := database.New("postgres://user:pass@localhost:5432/mydb?sslmode=disable")
if err != nil {
log.ErrorContext(ctx, "failed to connect to database", "error", err)
return
}
sessionDomain := session.New(db.Connection())
authDomain := auth.New(db.Connection(), sessionDomain.Service, "session_id", nil, nil, nil)
app := application.New()
app.RegisterDatabase("main", db)
app.RegisterDomain("session", "main", sessionDomain)
app.RegisterDomain("auth", "main", authDomain)
api := httpserver.New("8080", 3*time.Second)
api.Use(log.NewTraceIDMiddleware(nil, ""))
api.Use(httpserver.NewRecoverMiddleware())
api.HandleGroup("/auth", authDomain.HandleGroup)
protected := httpserver.NewHandlerGroup()
protected.Use(authDomain.Middleware)
protected.HandleFunc("/profile", func(w http.ResponseWriter, r *http.Request) {
user := auth.UserFromContext(r.Context())
w.Write([]byte("Welcome, " + user.Username))
})
api.HandleGroup("/api", protected)
api.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
})
app.RegisterService("api", api)
if err := app.Run(ctx); err != nil {
log.ErrorContext(ctx, "app finished with error", "error", err)
}
}
```
# database
The `database` package provides PostgreSQL connection management with built-in migration support using sqlx.
Core Components:
* `Database`: Database connection with migration orchestration capabilities.
* `Migration`: Represents a single database migration with Up and Down SQL statements.
* `New(connection string) (*Database, error)`: Creates a new PostgreSQL database connection.
* `ParseMigrations(fsys fs.FS) ([]Migration, error)`: Parses SQL migration files from a filesystem.
[Full package docs at pkg.go.dev](https://pkg.go.dev/github.com/platforma-dev/platforma/database)
## Step-by-step guide
[Section titled “Step-by-step guide”](#step-by-step-guide)
1. Create a database connection
```go
db, err := database.New("postgres://user:password@localhost:5432/mydb?sslmode=disable")
if err != nil {
log.ErrorContext(ctx, "failed to connect", "error", err)
os.Exit(1)
}
```
This establishes a PostgreSQL connection using the provided connection string.
2. Define your repository with migrations
```go
type UserRepository struct {
db *sqlx.DB
}
//go:embed *.sql
var migrations embed.FS
func (r *UserRepository) Migrations() fs.FS {
return migrations
}
```
The `Migrations()` method returns a filesystem containing SQL migration files.
3. Create SQL migration files
```sql
-- 001_create_users_table.sql
-- +migrate Up
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
);
-- +migrate Down
DROP TABLE users;
```
Migration files use `-- +migrate Up` and `-- +migrate Down` markers to define schema changes.
4. Register your repository
```go
userRepo := NewUserRepository(db.Connection())
db.RegisterRepository("users", userRepo)
```
Repositories that implement the `Migrations()` method returning `fs.FS` will automatically be registered for migration.
5. Run migrations
```go
err = db.Migrate(ctx)
if err != nil {
log.ErrorContext(ctx, "failed to run migrations", "error", err)
os.Exit(1)
}
```
This applies all pending migrations and tracks them in the `platforma_migrations` table.
6. Use the database connection
```go
var users []User
err := db.Connection().SelectContext(ctx, &users, "SELECT id, name, email FROM users")
```
Access the underlying `sqlx.DB` connection through the `Connection()` method.
## Using with Application
[Section titled “Using with Application”](#using-with-application)
The `Database` type does not implement the `Runner` interface. Instead, it’s registered with the application using the dedicated database registration methods:
```go
app := application.New()
// Register database connection
app.RegisterDatabase("main", db)
// Register repositories with the database
app.RegisterRepository("main", "users", userRepo)
// Register services
app.RegisterService("api", httpServer)
// Run the application (starts services)
app.Run(ctx)
```
Migrations are NOT run automatically when starting services. You must run migrations separately using the CLI:
```bash
# Run migrations first
myapp migrate
# Then start services
myapp run
```
If migrations fail, the application will exit with an error.
## Migration file format
[Section titled “Migration file format”](#migration-file-format)
Migration files use special markers to separate up and down SQL:
```sql
-- +migrate Up
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
-- +migrate Down
DROP TABLE users;
```
| Marker | Required | Description |
| ------------------ | -------- | ----------------------------------------- |
| `-- +migrate Up` | Yes | Marks the start of the up migration SQL |
| `-- +migrate Down` | No | Marks the start of the down migration SQL |
The migration ID is derived from the filename without the `.sql` extension. For example, `001_create_users.sql` becomes ID `001_create_users`.
## Migration tracking
[Section titled “Migration tracking”](#migration-tracking)
Migrations are tracked in the `platforma_migrations` table with three columns:
| Column | Description |
| ------------ | ------------------------------------------ |
| `repository` | Name used in `RegisterRepository` |
| `id` | Migration ID derived from the SQL filename |
| `timestamp` | When the migration was applied |
If a migration fails, previously applied migrations in the same batch are reverted using their `Down` SQL.
## Complete example
[Section titled “Complete example”](#complete-example)
001\_create\_users\_table.sql
```sql
-- +migrate Up
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
);
-- +migrate Down
DROP TABLE users;
```
main.go
```go
package main
import (
"context"
"embed"
"fmt"
"io/fs"
"os"
"github.com/jmoiron/sqlx"
"github.com/platforma-dev/platforma/database"
"github.com/platforma-dev/platforma/log"
)
// User represents a user in our system
type User struct {
ID int `db:"id"`
Name string `db:"name"`
Email string `db:"email"`
}
// UserRepository handles database operations for users
type UserRepository struct {
db *sqlx.DB
}
// NewUserRepository creates a new UserRepository with the given connection
func NewUserRepository(db *sqlx.DB) *UserRepository {
return &UserRepository{db: db}
}
//go:embed *.sql
var migrations embed.FS
func (r *UserRepository) Migrations() fs.FS {
return migrations
}
// Create inserts a new user into the database
func (r *UserRepository) Create(ctx context.Context, name, email string) (User, error) {
var user User
err := r.db.QueryRowxContext(ctx,
"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email",
name, email,
).StructScan(&user)
return user, err
}
// GetAll retrieves all users from the database
func (r *UserRepository) GetAll(ctx context.Context) ([]User, error) {
var users []User
err := r.db.SelectContext(ctx, &users, "SELECT id, name, email FROM users")
return users, err
}
func main() {
ctx := context.Background()
// Get database connection string from environment variable
// Example: "postgres://user:password@localhost:5432/mydb?sslmode=disable"
connStr := os.Getenv("DATABASE_URL")
if connStr == "" {
log.ErrorContext(ctx, "DATABASE_URL environment variable is not set")
os.Exit(1)
}
// Create new database connection
db, err := database.New(connStr)
if err != nil {
log.ErrorContext(ctx, "failed to connect to database", "error", err)
os.Exit(1)
}
log.InfoContext(ctx, "connected to database")
// Create repository and register it with the database
userRepo := NewUserRepository(db.Connection())
db.RegisterRepository("users", userRepo)
// Run migrations
err = db.Migrate(ctx)
if err != nil {
log.ErrorContext(ctx, "failed to run migrations", "error", err)
os.Exit(1)
}
log.InfoContext(ctx, "migrations completed successfully")
// Create a new user
user, err := userRepo.Create(ctx, "John Doe", "john@example.com")
if err != nil {
log.ErrorContext(ctx, "failed to create user", "error", err)
os.Exit(1)
}
log.InfoContext(ctx, "user created", "id", user.ID, "name", user.Name, "email", user.Email)
// Get all users
users, err := userRepo.GetAll(ctx)
if err != nil {
log.ErrorContext(ctx, "failed to get users", "error", err)
os.Exit(1)
}
fmt.Printf("Found %d user(s):\n", len(users))
for _, u := range users {
fmt.Printf(" - ID: %d, Name: %s, Email: %s\n", u.ID, u.Name, u.Email)
}
}
```
# httpserver
The `httpserver` package provides an HTTP server with middleware support, composable handler groups, and graceful shutdown.
Core Components:
* `HTTPServer`: HTTP server with middleware support and graceful shutdown. Implements `Runner` interface so it can be used as an `application` service.
* `HandlerGroup`: Composable group of HTTP handlers that share common middlewares. Implements `http.Handler` for nesting.
* `Middleware`: Interface for HTTP middleware with a `Wrap(http.Handler) http.Handler` method.
* `MiddlewareFunc`: Function type that implements `Middleware` for inline middleware definitions.
* `TraceIDMiddleware`: Adds a unique trace ID to request context and response headers.
* `RecoverMiddleware`: Catches panics in handlers and returns HTTP 500 responses.
* `FileServer`: Serves static files from an `fs.FS`. Implements `Runner` interface.
[Full package docs at pkg.go.dev](https://pkg.go.dev/github.com/platforma-dev/platforma/httpserver)
## Step-by-step guide
[Section titled “Step-by-step guide”](#step-by-step-guide)
1. Create a new HTTP server
```go
server := httpserver.New("8080", 3*time.Second)
```
The first argument is the port, the second is the shutdown timeout for graceful shutdown.
2. Register handlers
```go
server.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
})
server.Handle("/health", healthHandler)
```
Use `HandleFunc` for handler functions or `Handle` for `http.Handler` implementations.
3. Add middleware to the server
```go
server.Use(httpserver.NewTraceIDMiddleware(nil, ""))
server.Use(httpserver.NewRecoverMiddleware())
```
Middlewares are applied in the order they are added. `TraceIDMiddleware` adds a trace ID to all requests, `RecoverMiddleware` catches panics.
4. Create a handler group for nested routes
```go
apiGroup := httpserver.NewHandlerGroup()
apiGroup.HandleFunc("/users", usersHandler)
apiGroup.HandleFunc("/posts", postsHandler)
```
Handler groups let you organize routes with shared middleware.
5. Add middleware specific to the group
```go
apiGroup.UseFunc(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.InfoContext(r.Context(), "API request", "path", r.URL.Path)
h.ServeHTTP(w, r)
})
})
```
Use `UseFunc` to add inline middleware functions, or `Use` for `Middleware` implementations.
6. Mount the group on the server
```go
server.HandleGroup("/api", apiGroup)
```
The group is now accessible at `/api/users` and `/api/posts`. The path prefix is automatically stripped.
7. Run the server
```go
ctx := context.Background()
if err := server.Run(ctx); err != nil {
log.ErrorContext(ctx, "server error", "error", err)
}
```
Expected output:
```plaintext
time=2025-01-01T12:00:00.000+00:00 level=INFO msg="starting http server" address=:8080
```
Press Ctrl+C to trigger graceful shutdown. The server waits for in-flight requests up to the shutdown timeout.
## Using with Application
[Section titled “Using with Application”](#using-with-application)
Since `HTTPServer` implements the `Runner` interface, it can be registered as a service in an `Application`:
```go
app := application.New()
server := httpserver.New("8080", 3*time.Second)
server.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
})
app.RegisterService("api", server)
app.Run(ctx)
```
The server also implements `Healthchecker`, so its health status is automatically tracked by the application.
## Built-in middlewares
[Section titled “Built-in middlewares”](#built-in-middlewares)
### TraceIDMiddleware
[Section titled “TraceIDMiddleware”](#traceidmiddleware)
Adds a unique UUID trace ID to each request’s context and response headers.
```go
// Uses defaults: log.TraceIDKey for context, "Platforma-Trace-Id" header
server.Use(httpserver.NewTraceIDMiddleware(nil, ""))
// Custom context key and header
server.Use(httpserver.NewTraceIDMiddleware(myContextKey, "X-Request-ID"))
```
The trace ID is available in handlers via `r.Context().Value(log.TraceIDKey)`.
### RecoverMiddleware
[Section titled “RecoverMiddleware”](#recovermiddleware)
Catches panics in handlers, logs the error with request context, and returns HTTP 500.
```go
server.Use(httpserver.NewRecoverMiddleware())
```
## FileServer
[Section titled “FileServer”](#fileserver)
Serves static files from an `fs.FS` implementation:
```go
//go:embed static
var staticFiles embed.FS
fileServer := httpserver.NewFileServer(staticFiles, "/static", "8080")
app.RegisterService("files", fileServer)
```
## Complete example
[Section titled “Complete example”](#complete-example)
httpserver.go
```go
package main
import (
"context"
"net/http"
"time"
"github.com/platforma-dev/platforma/application"
"github.com/platforma-dev/platforma/httpserver"
"github.com/platforma-dev/platforma/log"
)
func main() {
ctx := context.Background()
// Initialize new application
app := application.New()
// Create HTTP server
api := httpserver.New("8080", 3*time.Second)
// Add /ping endpoint to `api`
api.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
})
// Add /long endpoint to HTTP server to test graceful shutdown
api.HandleFunc("/long", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(10 * time.Second)
w.Write([]byte("pong"))
})
// Add middleware to HTTP server. It will add trace ID to logs and responce headers
api.Use(log.NewTraceIDMiddleware(nil, ""))
// Create handler group
subApiGroup := httpserver.NewHandlerGroup()
// Add /clock endpoint to handler group
subApiGroup.HandleFunc("/clock", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(time.Now().String()))
})
// Add middleware to HTTP server. It will log all incoming requests to this handle group
subApiGroup.UseFunc(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.InfoContext(r.Context(), "incoming request", "addr", r.RemoteAddr)
h.ServeHTTP(w, r)
})
})
// Add handle group to HTTP server with /subApi path
api.HandleGroup("/subApi", subApiGroup)
// Register HTTP server as application server
app.RegisterService("api", api)
// Run application
if err := app.Run(ctx); err != nil {
log.ErrorContext(ctx, "app finished with error", "error", err)
}
// Now you can access http://localhost:8080/ping, http://localhost:8080/long
// and http://localhost:8080/subApi/clock URLs with GET method
}
```
# log
The `log` package provides structured application logs, HTTP trace propagation, and request-wide event logging with tail sampling.
Core Components:
* `Logger`, `SetDefault`, `Debug`/`Info`/`Warn`/`Error`: Package-level logging API built on top of `slog`.
* `New`: Builds a text or JSON logger that automatically extracts values like `traceId` and `serviceName` from `context.Context`.
* `TraceIDMiddleware`: Adds a per-request trace ID to context and response headers.
* `Event`: Mutable wide-event model with attrs, steps, errors, severity, and duration.
* `WideEventLogger`: Writes finalized `Event` values through a `Sampler`.
* `WideEventMiddleware`: Creates request-wide events, stores them in context, and emits them after handlers finish.
* `Sampler`, `SamplerFunc`, `DefaultSampler`: Tail-sampling rules for keeping errors, slow requests, selected status codes, and random samples.
* `EventFromContext`: Fetches the current request-wide event from context using `WideEventKey`.
[Full package docs at pkg.go.dev](https://pkg.go.dev/github.com/platforma-dev/platforma/log)
## Step-by-step guide
[Section titled “Step-by-step guide”](#step-by-step-guide)
1. Configure default structured logging
```go
logger := log.New(os.Stdout, "json", slog.LevelInfo, nil)
log.SetDefault(logger)
```
This switches package-level logging to JSON and keeps context extraction enabled.
2. Add trace IDs to each request
```go
server := httpserver.New("8080", 3*time.Second)
server.Use(log.NewTraceIDMiddleware(nil, ""))
```
With default arguments, the middleware stores IDs under `log.TraceIDKey` and writes `Platforma-Trace-Id` response headers.
3. Configure wide-event logging with sampling
```go
wideLogger := log.NewWideEventLogger(
os.Stdout,
log.NewDefaultSampler(2*time.Second, 500, 0.05),
"json",
nil,
)
server.Use(log.NewWideEventMiddleware(wideLogger, "", nil))
```
This keeps all error events, slow requests (>=2s), `5xx` responses, and 5% of the remaining traffic.
4. Enrich events and logs inside handlers
```go
server.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
log.InfoContext(r.Context(), "users request started")
if ev := log.EventFromContext(r.Context()); ev != nil {
ev.AddStep(slog.LevelInfo, "query users table")
ev.AddAttrs(map[string]any{"users.limit": 50})
}
w.WriteHeader(http.StatusOK)
})
```
`InfoContext` includes request metadata (for example `traceId`), and `EventFromContext` lets handlers attach detailed wide-event data.
5. Verify runtime output
```json
{"level":"INFO","msg":"users request started","traceId":"14b3..."}
{"level":"INFO","name":"http.request","duration":"12ms","request.status":200}
```
The first line is an immediate log entry. The second line is the finalized request-wide event emitted after the response is completed.
## Using with Application
[Section titled “Using with Application”](#using-with-application)
Integrate logging by registering an HTTP service and attaching `log` middlewares before `app.Run`:
```go
app := application.New()
server := httpserver.New("8080", 3*time.Second)
server.Use(log.NewTraceIDMiddleware(nil, ""))
wideLogger := log.NewWideEventLogger(
os.Stdout,
log.NewDefaultSampler(2*time.Second, 500, 0.05),
"json",
nil,
)
server.Use(log.NewWideEventMiddleware(wideLogger, "", nil))
server.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
log.InfoContext(r.Context(), "health check")
w.WriteHeader(http.StatusOK)
})
app.RegisterService("api", server)
app.Run(ctx)
```
## Complete example
[Section titled “Complete example”](#complete-example)
wide-events.go
```go
package main
import (
"context"
"errors"
"log/slog"
"os"
"time"
"github.com/platforma-dev/platforma/log"
)
func main() {
logger := log.NewWideEventLogger(
os.Stdout,
log.NewDefaultSampler(3*time.Second, 200, 0.1),
"json",
nil,
)
ev := log.NewEvent("test_event")
ev.AddStep(slog.LevelInfo, "some step")
ev.AddError(errors.New("some error"))
ev.AddAttrs(map[string]any{
"attr1": 1,
"attr2": true,
})
logger.WriteEvent(context.Background(), ev)
}
```
# queue
The `queue` package provides a generic, concurrent job processing system with worker pools and graceful shutdown.
Core Components:
* `Processor[T]`: Manages a pool of workers to process jobs from a queue. Implements `Runner` interface so it can be used as an `application` service.
* `Handler[T]`: Interface for processing jobs with a `Handle(ctx context.Context, job T)` method.
* `HandlerFunc[T]`: Function type that implements `Handler` for inline handler definitions.
* `Provider[T]`: Interface for queue implementations, allowing custom backends.
* `ChanQueue[T]`: Built-in thread-safe channel-based queue implementation.
* `ErrTimeout`: Error returned when an enqueue operation times out.
* `ErrClosedQueue`: Error returned when attempting to operate on a closed queue.
[Full package docs at pkg.go.dev](https://pkg.go.dev/github.com/platforma-dev/platforma/queue)
## Step-by-step guide
[Section titled “Step-by-step guide”](#step-by-step-guide)
1. Define your job type
```go
type job struct {
data int
}
```
Jobs can be any type. Use a struct for complex payloads.
2. Create a handler function
```go
func jobHandler(ctx context.Context, job job) {
log.InfoContext(ctx, "job handled", "data", job.data)
}
```
The handler receives the job and a context with worker ID for logging.
3. Create a queue
```go
q := queue.NewChanQueue[job](10, 3*time.Second)
```
First argument is the buffer size, second is the enqueue timeout. When the buffer is full, enqueue blocks until timeout.
4. Create a processor
```go
p := queue.New(queue.HandlerFunc[job](jobHandler), q, 2, time.Second)
```
Arguments: handler, queue provider, number of workers, shutdown timeout. The shutdown timeout allows workers to drain remaining jobs on shutdown.
5. Start the processor
```go
go p.Run(ctx)
```
Run blocks until context is cancelled and all workers complete. Use a goroutine for non-blocking operation.
Expected output:
```plaintext
time=2025-01-01T12:00:00.000+00:00 level=INFO msg="worker started" workerID=abc-123
time=2025-01-01T12:00:00.000+00:00 level=INFO msg="worker started" workerID=def-456
```
6. Enqueue jobs
```go
p.Enqueue(ctx, job{data: 1})
p.Enqueue(ctx, job{data: 2})
```
Jobs are distributed to available workers. Enqueue returns an error if the queue is full (timeout) or closed.
Expected output:
```plaintext
time=2025-01-01T12:00:00.001+00:00 level=INFO msg="job handled" workerID=abc-123 data=1
time=2025-01-01T12:00:00.001+00:00 level=INFO msg="job handled" workerID=def-456 data=2
```
## Using with Application
[Section titled “Using with Application”](#using-with-application)
Since `Processor` implements the `Runner` interface, it can be registered as a service in an `Application`:
```go
app := application.New()
q := queue.NewChanQueue[job](100, 5*time.Second)
p := queue.New(queue.HandlerFunc[job](jobHandler), q, 4, 10*time.Second)
app.RegisterService("queue", p)
app.Run(ctx)
```
The processor starts when the application runs and gracefully shuts down with it.
## Custom queue providers
[Section titled “Custom queue providers”](#custom-queue-providers)
Implement the `Provider` interface to use custom queue backends like Redis, RabbitMQ, or databases:
```go
type Provider[T any] interface {
Open(ctx context.Context) error
Close(ctx context.Context) error
EnqueueJob(ctx context.Context, job T) error
GetJobChan(ctx context.Context) (chan T, error)
}
```
The `Open` and `Close` methods handle connection lifecycle. `GetJobChan` returns the channel that workers read from.
## Error handling
[Section titled “Error handling”](#error-handling)
The package provides two error types for queue operations:
| Error | Condition |
| ---------------- | ----------------------------------------- |
| `ErrTimeout` | Enqueue operation timed out (buffer full) |
| `ErrClosedQueue` | Attempted operation on a closed queue |
Workers recover from panics automatically and log the error without crashing the processor.
## Complete example
[Section titled “Complete example”](#complete-example)
queue.go
```go
package main
import (
"context"
"time"
"github.com/platforma-dev/platforma/log"
"github.com/platforma-dev/platforma/queue"
)
type job struct {
data int
}
func jobHandler(ctx context.Context, job job) {
log.InfoContext(ctx, "job handled", "data", job.data)
}
func main() {
ctx := context.Background()
q := queue.NewChanQueue[job](10, 3*time.Second)
p := queue.New(queue.HandlerFunc[job](jobHandler), q, 2, time.Second)
go p.Run(ctx)
time.Sleep(time.Millisecond)
p.Enqueue(ctx, job{data: 1})
p.Enqueue(ctx, job{data: 2})
p.Enqueue(ctx, job{data: 3})
time.Sleep(time.Millisecond)
}
```
# scheduler
The `scheduler` package provides periodic task execution at fixed intervals.
Core Components:
* `Scheduler`: Executes a runner at configured intervals. Implements `Runner` interface so it can be used as an `application` service.
* `New(period, runner)`: Creates a new scheduler with the specified interval and runner.
[Full package docs at pkg.go.dev](https://pkg.go.dev/github.com/platforma-dev/platforma/scheduler)
## Step-by-step guide
[Section titled “Step-by-step guide”](#step-by-step-guide)
1. Create a task function
```go
func scheduledTask(ctx context.Context) error {
log.InfoContext(ctx, "scheduled task executed")
return nil
}
```
The function receives a context with a unique trace ID for each execution.
2. Create a scheduler
```go
s := scheduler.New(time.Second, application.RunnerFunc(scheduledTask))
```
First argument is the interval between executions. Second is any `application.Runner` implementation. Use `application.RunnerFunc` to wrap a function.
3. Run the scheduler
```go
err := s.Run(ctx)
```
The scheduler blocks and executes the task at each interval until the context is cancelled.
Expected output:
```plaintext
time=2025-01-01T12:00:01.000+00:00 level=INFO msg="scheduler task started" traceID=abc-123
time=2025-01-01T12:00:01.000+00:00 level=INFO msg="scheduled task executed" traceID=abc-123
time=2025-01-01T12:00:01.000+00:00 level=INFO msg="scheduler task finished" traceID=abc-123
time=2025-01-01T12:00:02.000+00:00 level=INFO msg="scheduler task started" traceID=def-456
time=2025-01-01T12:00:02.000+00:00 level=INFO msg="scheduled task executed" traceID=def-456
time=2025-01-01T12:00:02.000+00:00 level=INFO msg="scheduler task finished" traceID=def-456
```
4. Handle errors
```go
func scheduledTask(ctx context.Context) error {
if err := doWork(); err != nil {
return err // Logged automatically, scheduler continues
}
return nil
}
```
Errors returned from the task are logged but do not stop the scheduler. The next execution proceeds as normal.
## Using with Application
[Section titled “Using with Application”](#using-with-application)
Since `Scheduler` implements the `Runner` interface, it can be registered as a service in an `Application`:
```go
app := application.New()
s := scheduler.New(time.Minute, application.RunnerFunc(scheduledTask))
app.RegisterService("scheduler", s)
app.Run(ctx)
```
The scheduler starts when the application runs and stops when the application shuts down.
## Complete example
[Section titled “Complete example”](#complete-example)
scheduler.go
```go
package main
import (
"context"
"time"
"github.com/platforma-dev/platforma/application"
"github.com/platforma-dev/platforma/log"
"github.com/platforma-dev/platforma/scheduler"
)
func scheduledTask(ctx context.Context) error {
log.InfoContext(ctx, "scheduled task executed")
return nil
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
s := scheduler.New(time.Second, application.RunnerFunc(scheduledTask))
go func() {
time.Sleep(3500 * time.Millisecond)
cancel()
}()
s.Run(ctx)
}
```