Saltar al contenido principal

Storage & background tasks

File storage (pkg/storage)

pkg/storage is a provider-agnostic file storage abstraction with a durable interface designed to last through v1.x. The same code runs against:

  • the local filesystem,
  • AWS S3,
  • Google Cloud Storage,
  • Azure Blob Storage.
import "github.com/jcsvwinston/nucleus/pkg/storage"

// Get returns a ReadCloser and object metadata; always close the reader.
reader, info, err := a.Storage.Get(ctx, "uploads/avatar.png")

// SignedURL requires an opts argument (use zero value for defaults).
url, err := a.Storage.SignedURL(ctx, "uploads/avatar.png", 5*time.Minute, storage.URLConfig{})

// Put returns the stored ObjectInfo and an error.
info, err = a.Storage.Put(ctx, "uploads/avatar.png", body, storage.PutOptions{
ContentType: "image/png",
})
_ = reader
_ = info

Configure the backend in nucleus.yml. The exact key shape is provider- specific — the snippet below is illustrative; the canonical schema lives in docs/reference/CONFIG_KEY_REGISTRY.md and follows the layout in SPEC.md §3.8:

# illustrative — see CONFIG_KEY_REGISTRY.md for the full storage.* schema
storage:
provider: s3 # local | s3 | gcs | azure
s3:
bucket: my-bucket
region: eu-west-1

Per-driver credentials and endpoints are read from environment variables or platform credential providers — never embedded in the config file.

The admin panel surfaces a file browser against the configured backend.

Circuit breaker (storage)

App.New automatically wraps all remote provider operations (Put, Get, Delete, Exists, List, Copy, SignedURL) with a pkg/circuit.Breaker. The local provider and PublicURL (pure string composition) are never wrapped. storage.ErrNotFound is not counted as a failure — a missing object is a normal outcome.

When the breaker is open, wrapped operations return circuit.ErrOpen immediately. The default thresholds are:

storage:
circuit_breaker:
enabled: true
failure_threshold: 5
cooldown: 30s
half_open_max_concurrent: 1

Set enabled: false to disable, or tune the thresholds for your workload. Full details: docs/guides/STORAGE_GUIDE.md.

Background tasks (pkg/tasks)

pkg/tasks runs background jobs on Asynq + Redis. Payloads are encoded as JSON and keyed by a task-type string; the framework handles enqueue, retry, dead-letter and metrics.

tasks.Manager is an interface constructed by the application's task wiring — it is not exposed as a field on App. Hold the Manager your wiring builds and use it directly:

import (
"context"

"github.com/jcsvwinston/nucleus/pkg/tasks"
)

const TypeSendWelcomeEmail = "email:welcome"

type SendWelcomeEmail struct {
UserID int64
}

// mgr is a tasks.Manager held by your task wiring.
// Register a handler for the task type. tasks.HandlerFunc is
// func(ctx context.Context, task tasks.Task) error.
mgr.HandleFunc(TypeSendWelcomeEmail, tasks.HandlerFunc(
func(ctx context.Context, task tasks.Task) error {
var payload SendWelcomeEmail
if err := tasks.DecodeJSONPayload(task, &payload); err != nil {
return err
}
return sendWelcome(ctx, payload.UserID)
},
))

// Enqueue from a request handler (payload is JSON-encoded for you):
id, err := mgr.EnqueueJSON(TypeSendWelcomeEmail, SendWelcomeEmail{UserID: 42})

The admin panel exposes the queue inspector — pending, in-flight, retried, dead-lettered — with one-click requeue.

Transactional outbox (pkg/outbox)

The naïve "enqueue inside a SQL transaction" pattern silently loses events when the transaction commits but the queue write fails. pkg/outbox solves this with the standard outbox pattern:

import (
"database/sql"

"github.com/jcsvwinston/nucleus/pkg/outbox"
)

// App.DB.Tx runs fn inside a transaction (tx is a *sql.Tx).
// App.Outbox is a *outbox.ManagedOutbox; EnqueueTx writes the event row
// in the SAME transaction, so the event is durable iff the commit lands.
err := a.DB.Tx(ctx, func(tx *sql.Tx) error {
if err := repo.Save(tx, article); err != nil {
return err
}
_, err := a.Outbox.EnqueueTx(ctx, tx, outbox.Entry{
Topic: "article.published",
Payload: ArticlePublished{ID: article.ID},
})
return err
})

The outbox table is part of the migration set the framework manages. A relay process (run inline in development, separately in production) moves committed events into the task queue.

Mail (pkg/mail)

Two drivers ship out of the box:

DriverUse
noopTests and development — captures payloads in memory.
smtpAnything that speaks SMTP.

Vendor-specific HTTP providers (SendGrid, Mailgun, AWS SES, Postmark, Resend, …) install as nucleus-plugin-<provider> binaries on PATH and are discovered via the capability-style external bridge (pkg/plugins). The mail.send capability contract is documented in the Plugin SDK reference; a runnable reference skeleton returns with the v0.9.X reference applications.

Circuit breaker (mail)

App.New automatically wraps mail.Sender.Send with a pkg/circuit.Breaker. The noop driver and the Healthy SMTP HELO probe (used by /healthz) are never wrapped — so health checks can observe that a mail relay has recovered while Send is still short-circuited.

When the breaker is open, Send returns circuit.ErrOpen. The default thresholds are:

mail_circuit_breaker:
enabled: true
failure_threshold: 5
cooldown: 30s
half_open_max_concurrent: 1

Set enabled: false to disable. Config keys are documented in docs/reference/CONFIG_KEY_REGISTRY.md.