Skip to content
ArenileArenile
Coste360.itCoste360.it

Extensions (pkg/ext)

The interfaces of the pkg/ext package are the only stable Go APIs for those who extend the core: changing them is a breaking change. Everything in internal/ can change without notice. pkg/ext imports nothing from internal/, so an extension depends on public types only.

type Extensions struct {
PaymentProviders []PaymentProvider // added to "manual" and "stripe"; same Code = replaces
FiscalDrivers []FiscalDriver // added to "none"
EventSinks []EventSink // receive every event after the outbox (at least once)
ConcessionData ConcessionDataProvider // perimeter, imagery, expiry of the concession
TenantResolver TenantResolver // establishment from host/header (custom domains)
AuthProviders []AuthProvider // external sign-in, e.g. OIDC
Notifiers []Notifier // channels beyond email: SMS, WhatsApp
BotProtection BotProtection // invisible challenge on public creations
EventBus EventBus // real-time bus, e.g. PostgreSQL LISTEN/NOTIFY
HTTPRoutes func(r chi.Router) // extra routes (not on /api/admin/v1, /api/public/v1, /admin, /b)
AdminUIPlugins []UIPluginManifest // menu entries and pages in the panel
}

The official binary uses only the core implementations. To add yours, build your own main that registers them:

cmd/my-arenile/main.go
package main
import (
"context"
"log"
"github.com/coste360/arenile/internal/app"
"github.com/coste360/arenile/internal/config"
"github.com/coste360/arenile/pkg/ext"
"example.com/arenile-satispay"
)
func main() {
cfg, err := config.FromEnv()
if err != nil {
log.Fatal(err)
}
a, err := app.New(cfg, app.WithExtensions(ext.Extensions{
PaymentProviders: []ext.PaymentProvider{satispay.New()},
}))
if err != nil {
log.Fatal(err)
}
defer a.Close()
if err := a.Run(context.Background()); err != nil {
log.Fatal(err)
}
}

PaymentProvider

Payment driver: Code, Capabilities, CreatePayment, ParseWebhook, Refund. See Payment drivers.

FiscalDriver

Issuing commercial documents: Code, Issue, Void, Status. Lines come from the price snapshot (gross amounts, rates in basis points: 2200 = 22%).

EventSink

Name, Handle(ctx, Event): receives every domain event after the outbox. Must be idempotent on the id.

EventBus

Publish, Subscribe: replaces the in-memory real-time bus, for several instances in parallel.

Notifier

Channel, Send: new notification channels alongside email.

AuthProvider

Code, DisplayName, BeginLogin, CompleteLogin: sign-in with an external identity provider.

TenantResolver

ResolveSlug(r): resolves the public page’s establishment from the domain (e.g. prenota.lido-a.it).

ConcessionDataProvider

Lookup(ctx, ref): state-owned beach concession data from external sources.

BotProtection

ClientConfig, Verify: invisible challenge with no imposed service.

UIPluginManifest

Menu entries (labels per language, Italian required) and a page URL loaded in a dedicated area of the panel.

type Event struct {
ID string `json:"id"` // "evt_<uuidv7>"
Type string `json:"type"` // e.g. "booking.confirmed"
EstablishmentID string `json:"establishment_id"`
OccurredAt time.Time `json:"occurred_at"`
Data json.RawMessage `json:"data"` // versioned payload
Version int `json:"version"`
}

The list of types is in Events and outgoing webhooks.

  • int64 amounts in cents, never float.
  • No personal data, tokens or secrets in logs.
  • Credentials arrive already decrypted in the call: do not store them elsewhere.
  • A slow EventSink does not block the core, but it must handle its own retries and idempotency.
  • New dependencies must have a license compatible with BSL 1.1 and AGPL-3.0 (MIT, BSD, Apache-2.0, ISC).