Skip to content

Interfaces

ss-keel-core is built around interfaces. The core package defines the contracts; you provide the implementation (or use an official addon when available).


Generic CRUD contract for data access. T is your entity type, ID is the identifier type.

type Repository[T any, ID any] interface {
FindByID(ctx context.Context, id ID) (*T, error)
FindAll(ctx context.Context, q PageQuery) (Page[T], error)
Create(ctx context.Context, entity *T) error
Update(ctx context.Context, id ID, entity *T) error
Delete(ctx context.Context, id ID) error
}
type UserRepository interface {
core.Repository[User, string]
}
// Implementation
type PostgresUserRepository struct{ db *sql.DB }
func (r *PostgresUserRepository) FindByID(ctx context.Context, id string) (*User, error) {
// SELECT * FROM users WHERE id = $1
}
func (r *PostgresUserRepository) FindAll(ctx context.Context, q core.PageQuery) (core.Page[User], error) {
offset := (q.Page - 1) * q.Limit
// SELECT * FROM users LIMIT $1 OFFSET $2
}
// ... Create, Update, Delete

Key-value cache interface.

type Cache interface {
Get(ctx context.Context, key string) ([]byte, error)
Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
Delete(ctx context.Context, key string) error
Exists(ctx context.Context, key string) (bool, error)
}
type UserService struct {
cache core.Cache
repo UserRepository
}
func (s *UserService) GetByID(ctx context.Context, id string) (*User, error) {
data, err := s.cache.Get(ctx, "user:"+id)
if err == nil {
var user User
json.Unmarshal(data, &user)
return &user, nil
}
user, err := s.repo.FindByID(ctx, id)
if err != nil {
return nil, err
}
b, _ := json.Marshal(user)
s.cache.Set(ctx, "user:"+id, b, 5*time.Minute)
return user, nil
}

Authentication/authorization middleware.

type Guard interface {
Middleware() fiber.Handler
}
type JWTGuard struct{ secret string }
func (g *JWTGuard) Middleware() fiber.Handler {
return func(c *fiber.Ctx) error {
token := c.Get("Authorization")
claims, err := verifyJWT(token, g.secret)
if err != nil {
return core.Unauthorized("invalid token")
}
ctx := &core.Ctx{Ctx: c}
ctx.SetUser(claims)
return c.Next()
}
}

See Authentication for full usage.


Async messaging interfaces.

type Message struct {
Topic string
Key []byte
Payload []byte
Headers map[string]string
}
type MessageHandler func(ctx context.Context, msg Message) error
type Publisher interface {
Publish(ctx context.Context, msg Message) error
Close() error
}
type Subscriber interface {
Subscribe(ctx context.Context, topic string, handler MessageHandler) error
Close() error
}
// Publishing
func (s *OrderService) Create(ctx context.Context, order *Order) error {
if err := s.repo.Create(ctx, order); err != nil {
return err
}
payload, _ := json.Marshal(order)
return s.publisher.Publish(ctx, core.Message{
Topic: "orders.created",
Key: []byte(order.ID),
Payload: payload,
})
}
// Subscribing
s.subscriber.Subscribe(ctx, "orders.created", func(ctx context.Context, msg core.Message) error {
var order Order
json.Unmarshal(msg.Payload, &order)
return s.sendConfirmationEmail(ctx, &order)
})

Email sending interface.

type MailAttachment struct {
Filename string
ContentType string
Data []byte
}
type Mail struct {
From string
To []string
CC []string
BCC []string
Subject string
HTMLBody string
TextBody string
Attachments []MailAttachment
}
type Mailer interface {
Send(ctx context.Context, mail Mail) error
}
func (s *AuthService) SendPasswordReset(ctx context.Context, user *User, token string) error {
return s.mailer.Send(ctx, core.Mail{
From: "noreply@example.com",
To: []string{user.Email},
Subject: "Reset your password",
HTMLBody: buildResetEmailHTML(user.Name, token),
TextBody: buildResetEmailText(user.Name, token),
})
}

Object storage interface (e.g., S3, GCS, local disk).

type StorageObject struct {
Key string
Size int64
ContentType string
LastModified time.Time
}
type Storage interface {
Put(ctx context.Context, key string, r io.Reader, size int64, contentType string) error
Get(ctx context.Context, key string) (io.ReadCloser, error)
Delete(ctx context.Context, key string) error
URL(ctx context.Context, key string, expiry time.Duration) (string, error)
Stat(ctx context.Context, key string) (*StorageObject, error)
}
func (s *AvatarService) Upload(ctx context.Context, userID string, file io.Reader, size int64) (string, error) {
key := fmt.Sprintf("avatars/%s.jpg", userID)
if err := s.storage.Put(ctx, key, file, size, "image/jpeg"); err != nil {
return "", core.Internal("failed to upload avatar", err)
}
url, err := s.storage.URL(ctx, key, 24*time.Hour)
if err != nil {
return "", err
}
return url, nil
}

Cron job scheduler interface.

type Job struct {
Name string
Schedule string // cron expression, e.g. "*/5 * * * *"
Handler func(ctx context.Context) error
}
type Scheduler interface {
Add(job Job) error
Start()
Stop(ctx context.Context)
}
app.RegisterScheduler(scheduler)

RegisterScheduler automatically wires scheduler.Stop(ctx) as a shutdown hook.

scheduler.Add(core.Job{
Name: "cleanup-expired-sessions",
Schedule: "0 * * * *", // every hour
Handler: func(ctx context.Context) error {
return sessionRepo.DeleteExpired(ctx)
},
})
scheduler.Add(core.Job{
Name: "send-weekly-digest",
Schedule: "0 9 * * 1", // every Monday at 9am
Handler: func(ctx context.Context) error {
return mailer.SendWeeklyDigest(ctx)
},
})
scheduler.Start()
app.RegisterScheduler(scheduler)

Hook into the request lifecycle to record metrics.

type RequestMetrics struct {
Method string
Path string
StatusCode int
Duration time.Duration
}
type MetricsCollector interface {
RecordRequest(m RequestMetrics)
}
app.SetMetricsCollector(prometheusCollector)
type PrometheusCollector struct {
histogram *prometheus.HistogramVec
}
func (c *PrometheusCollector) RecordRequest(m core.RequestMetrics) {
c.histogram.WithLabelValues(
m.Method,
m.Path,
strconv.Itoa(m.StatusCode),
).Observe(m.Duration.Seconds())
}

Distributed tracing interface compatible with OpenTelemetry patterns.

type Span interface {
SetAttribute(key string, value any)
RecordError(err error)
End()
}
type Tracer interface {
Start(ctx context.Context, name string) (context.Context, Span)
}

A no-op tracer is used by default — zero overhead if you don’t set one.

app.SetTracer(otelTracer)
tracer := app.Tracer()
func (s *UserService) GetByID(ctx context.Context, id string) (*User, error) {
ctx, span := tracer.Start(ctx, "UserService.GetByID")
defer span.End()
user, err := s.repo.FindByID(ctx, id)
if err != nil {
span.RecordError(err)
return nil, err
}
span.SetAttribute("user.id", id)
return user, nil
}

Internationalization interface for translating strings.

type Translator interface {
T(locale, key string, args ...any) string
Locales() []string
}
app.SetTranslator(i18nTranslator)
func (c *UserController) create(ctx *core.Ctx) error {
msg := ctx.T("users.created_successfully")
return ctx.Created(map[string]string{"message": msg})
}

ctx.T(key, args...) reads the locale from the Accept-Language header automatically.

See Ctx.T and Ctx.Lang for details.