Extensions Go PocketBase : Hooks, Tests et Templates 2026

Extensions Go PocketBase : Hooks, Tests et Templates 2026

Introduction

PocketBase est un backend open-source léger qui s'exécute en un seul fichier binaire Go. Pour les startups et les PME qui cherchent à construire des applications scalables sans équipe backend dédiée, il offre une base solide avec base de données SQLite intégrée, API REST, authentification et realtime. Mais quand vos besoins dépassent les fonctionnalités par défaut – par exemple, pour intégrer un service de paiement comme Stripe ou générer des PDF dynamiques –, les extensions en Go deviennent essentielles.

Ce guide s'adresse aux développeurs débutants en Go ou aux équipes de startups qui veulent personnaliser PocketBase sans repartir de zéro. Nous couvrons la création d'une app Go personnalisée, l'utilisation des hooks pour intercepter les événements, les tests unitaires et d'intégration, ainsi que le rendu de templates HTML pour les pages web ou les emails. Tout est basé sur la documentation officielle de PocketBase (version 0.23+ en 2025), avec des exemples concrets que vous pouvez copier-coller.

À la fin, vous saurez compiler un exécutable unique qui inclut votre logique métier, prêt pour le déploiement sur un VPS ou en conteneur Docker. Prévoyez 30 minutes pour tester le premier exemple.

1. Créer une app Go personnalisée

Pour étendre PocketBase en Go, commencez par l'importer comme un package standard. Cela vous permet de créer une application portable qui inclut le serveur, la base de données et votre code custom, le tout dans un binaire statique de quelques mégaoctets.

Installez Go 1.23 ou supérieur (disponible sur golang.org). Créez un nouveau projet :

mkdir mon-app-pocketbase
cd mon-app-pocketbase
go mod init monapp
go get github.com/pocketbase/pocketbase

Créez un fichier main.go avec cet exemple minimal :

package main

import (
    "log"
    "os"
    "github.com/pocketbase/pocketbase"
    "github.com/pocketbase/pocketbase/apis"
    "github.com/pocketbase/pocketbase/core"
)

func main() {
    app := pocketbase.New()

    // Hook pour servir des fichiers statiques depuis pb_public (optionnel)
    app.OnServe().BindFunc(func(se *core.ServeEvent) error {
        se.Router.GET("/{path...}", apis.Static(os.DirFS("./pb_public"), false))
        return se.Next()
    })

    if err := app.Start(); err != nil {
        log.Fatal(err)
    }
}

Exécutez avec go run . serve. PocketBase démarre sur http://127.0.0.1:8090, avec l'interface admin accessible après création du premier utilisateur. Pour un build statique (idéal pour les déploiements sans dépendances) :

go build -ldflags="-s -w" -o monapp
./monapp serve

Le binaire résultant (~11 MB) est autonome et peut tourner sur n'importe quel serveur Linux sans installer Go.

Configuration de la base de données

Par défaut, PocketBase utilise un driver SQLite pur Go (modernc.org/sqlite), sans CGO, ce qui réduit la taille du binaire. Pour des besoins avancés comme le support FTS5 (recherche full-text) ou ICU (collation internationale), configurez un driver custom via DBConnect dans pocketbase.Config.

Exemple avec github.com/mattn/go-sqlite3 (driver CGO pour plus de performances) :

package main

import (
    "database/sql"
    "log"
    "github.com/mattn/go-sqlite3"
    "github.com/pocketbase/dbx"
    "github.com/pocketbase/pocketbase"
)

func init() {
    sql.Register("pb_sqlite3", &sqlite3.SQLiteDriver{
        ConnectHook: func(conn *sqlite3.SQLiteConn) error {
            _, err := conn.Exec(`
                PRAGMA busy_timeout = 10000;
                PRAGMA journal_mode = WAL;
                PRAGMA journal_size_limit = 200000000;
                PRAGMA synchronous = NORMAL;
                PRAGMA foreign_keys = ON;
                PRAGMA temp_store = MEMORY;
                PRAGMA cache_size = -32000;
            `, nil)
            return err
        },
    })
    dbx.BuilderFuncMap["pb_sqlite3"] = dbx.BuilderFuncMap["sqlite3"]
}

func main() {
    app := pocketbase.NewWithConfig(pocketbase.Config{
        DBConnect: func(dbPath string) (*dbx.DB, error) {
            return dbx.Open("pb_sqlite3", dbPath)
        },
    })

    if err := app.Start(); err != nil {
        log.Fatal(err)
    }
}

Compilez avec CGO_ENABLED=1 go build. Pour réduire la taille sans le driver par défaut, ajoutez -tags no_default_driver. Utilisez core.DefaultDBConnect comme fallback si le custom échoue.

Pour une startup, commencez avec le driver par défaut : il suffit pour 90 % des cas, comme une app de gestion de clients avec 10 000 records.

2. Hooks : OnBeforeServe, OnRecordAfterCreate et plus

Les hooks sont le cœur des extensions Go. Ils interceptent les événements de l'app (lifecycle, CRUD, auth) pour ajouter de la logique sans modifier le code source de PocketBase. Chaque hook est une chaîne de handlers : appelez e.Next() pour passer au suivant, ou retournez une erreur pour stopper.

Enregistrez-les via app.On[HookName]().BindFunc(func(e *EventType) error { ... }). Utilisez e.App pour accéder à l'app (évite les deadlocks en transaction). Évitez les verrous globaux pour prévenir les boucles récursives.

Hooks au niveau App

Ces hooks gèrent le cycle de vie global.

  • OnBootstrap() : Initialisation (DB, settings). Pas d'accès DB avant.
app.OnBootstrap().BindFunc(func(e *core.BootstrapEvent) error {
    // Configurez des ressources ici, comme charger des configs externes
    return e.Next()
})
  • OnServe() : Après démarrage TCP, avant le bloc serve. Parfait pour ajouter des routes custom.
app.OnServe().BindFunc(func(e *core.ServeEvent) error {
    e.Router.GET("/api/hello", func(re *core.RequestEvent) error {
        return re.String(200, "Hello depuis une route custom !")
    }).Bind(apis.RequireSuperuser()) // Middleware auth superuser

    // Servir un SPA React depuis pb_public
    e.Router.GET("/app/{path...}", apis.Static(os.DirFS("./pb_public"), true))
    return e.Next()
})
  • OnTerminate() : À la fermeture (SIGTERM). Nettoyez les ressources.
app.OnTerminate().BindFunc(func(e *core.TerminateEvent) error {
    // Sauvegardez des états ou fermez des connexions externes
    if e.IsRestart {
        log.Println("Redémarrage en cours")
    }
    return e.Next()
})
  • OnMailerSend() : Avant envoi d'email. Modifiez le sujet ou le corps.
app.OnMailerSend().BindFunc(func(e *core.MailerEvent) error {
    e.Message.Subject = "Votre mise à jour PocketBase"
    return e.Next()
})

Pour les emails auth spécifiques (reset mot de passe, OTP) : OnMailerRecordPasswordResetSend(), etc.

  • Hooks Realtime : OnRealtimeConnectRequest(), OnRealtimeSubscribeRequest(), OnRealtimeMessageSend() pour filtrer les connexions SSE.

Hooks pour Records et Collections

Ces hooks s'appliquent aux opérations CRUD. Utilisez-les pour valider, enrichir ou logger.

  • OnRecordEnrich("collection") : Ajoutez/masquez des champs avant réponse (API, realtime).
app.OnRecordEnrich("users").BindFunc(func(e *core.RecordEnrichEvent) error {
    e.Record.Hide("internal_id") // Masque un champ sensible
    if e.RequestInfo != nil && e.RequestInfo.Auth != nil {
        // Calculez un score basé sur l'utilisateur connecté
        score := e.Record.GetInt("points") * e.RequestInfo.Auth.GetInt("multiplier")
        e.Record.Set("computed_score", score)
    }
    return e.Next()
})
  • Validation : OnRecordValidate() avant Save() ou Validate().
app.OnRecordValidate("posts").BindFunc(func(e *core.RecordEvent) error {
    if e.Record.GetString("title") == "" {
        return errors.New("Le titre est obligatoire")
    }
    return e.Next()
})
  • Hooks CRUD : Pour create/update/delete, il y a des phases pré/post, execute/success/error.

Exemple pour after create :

app.OnRecordAfterCreateSuccess("orders").BindFunc(func(e *core.RecordEvent) error {
    // Intégrez Stripe : créez un paiement après persistance DB
    record := e.Record
    amount := record.GetFloat("total")
    // Code Stripe : stripe.PaymentIntent.Create(...)
    log.Printf("Paiement créé pour commande %s : %.2f €", record.Id, amount)
    return e.Next()
})

Pour update/delete, suivez le même pattern : OnRecordUpdateExecute(), OnRecordAfterUpdateError(), etc. Les hooks "AfterSuccess" sont transaction-safe (exécutés après commit).

Pour collections (gestion des schémas) : OnCollectionAfterCreateSuccess(), etc.

Hooks pour Requêtes API

Ces hooks ont un contexte HTTP (e.HttpContext).

  • OnRecordCreateRequest("users") : Avant création via API.
app.OnRecordCreateRequest("users").BindFunc(func(e *core.RecordRequestEvent) error {
    // Loggez l'IP de création
    log.Printf("Nouvel user depuis %s", e.HttpContext.RealIP())
    return e.Next()
})
  • Auth : OnRecordAuthWithPasswordRequest() pour customiser le token après login.

Pour une PME, utilisez ces hooks pour logger les audits ou intégrer des services tiers comme SendGrid pour les emails ou Twilio pour les SMS.

3. Tests unitaires

Tester vos extensions Go assure la fiabilité, crucial pour les startups où un bug peut bloquer un lancement. PocketBase fournit tests.TestApp pour simuler une app en mémoire, et tests.ApiScenario pour tester les endpoints API sans serveur réel.

Préparez des données de test : Lancez une instance avec --dir="./test_pb_data" --automigrate=0, créez collections/records via l'admin UI, puis commitez le dossier test_pb_data dans votre repo.

Exemple de test pour une route /api/hello qui requiert superuser :

D'abord, dans main.go, ajoutez la route via OnServe() (comme ci-dessus).

Créez main_test.go :

package main

import (
    "testing"
    "github.com/pocketbase/pocketbase/tests"
    "github.com/stretchr/testify/require"
)

func generateToken(t *testing.T, app *tests.TestApp, email string) string {
    record, err := app.Dao().FindFirstRecordByData("users", "email", email)
    require.NoError(t, err)
    token, err := record.NewAuthToken("test", "test")
    require.NoError(t, err)
    return token
}

func TestHelloEndpoint(t *testing.T) {
    scenario := &tests.ApiScenario{
        Method:          "GET",
        Url:             "/api/hello",
        ExpectedStatus:  405, // Test POST invalide
    }
    scenario.Test(t)

    // Test guest
    scenario = &tests.ApiScenario{
        Method:          "GET",
        Url:             "/api/hello",
        ExpectedStatus:  401,
    }
    scenario.Test(t)

    // Test user normal
    scenario = &tests.ApiScenario{
        Method:          "GET",
        Url:             "/api/hello",
        ExpectedStatus:  401,
        Headers: map[string]string{
            "Authorization": "Bearer " + generateToken(t, scenario.TestAppFactory(), "[email protected]"),
        },
    }
    scenario.Test(t)

    // Test superuser
    scenario = &tests.ApiScenario{
        Method:          "GET",
        Url:             "/api/hello",
        ExpectedStatus:  200,
        ExpectedContent: []byte("Hello world!"),
        Headers: map[string]string{
            "Authorization": "Bearer " + generateToken(t, scenario.TestAppFactory(), "[email protected]"),
        },
        TestAppFactory: func() *tests.TestApp {
            app := tests.NewTestApp("test_pb_data")
            bindAppHooks(app) // Fonction pour lier vos hooks
            return app
        },
    }
    scenario.Test(t)
}

func bindAppHooks(app core.App) {
    // Liez vos hooks ici pour les tests
    app.OnServe().BindFunc(func(e *core.ServeEvent) error {
        // Route de test
        e.Router.GET("/api/hello", func(re *core.RequestEvent) error {
            return re.String(200, "Hello world!")
        }).Bind(apis.RequireSuperuser())
        return e.Next()
    })
}

Exécutez go test ./.... ApiScenario gère le setup/teardown, y compris les headers et assertions. Pour les mocks, importez github.com/pocketbase/pocketbase/tests – il inclut MockMultipartData pour tester les uploads.

Pour les hooks sans API, testez-les directement avec TestApp :

func TestAfterCreateHook(t *testing.T) {
    app := tests.NewTestApp("test_pb_data")
    app.OnRecordAfterCreateSuccess("users").BindFunc(func(e *core.RecordEvent) error {
        e.Record.Set("tested", true)
        return e.Next()
    })

    record := core.NewRecord(app.Dao(), "users")
    record.Set("email", "[email protected]")
    require.NoError(t, app.Dao().SaveRecord(record))

    loaded, err := app.Dao().FindRecordById("users", record.Id)
    require.NoError(t, err)
    require.True(t, loaded.GetBool("tested"))
}

Ces outils simplifient les tests pour les PME : pas besoin de Selenium, juste du Go natif.

4. Templates HTML et Email

PocketBase utilise le package html/template de Go pour rendre des templates sécurisés (échappement auto contre XSS). Intégrez-les via des routes custom pour servir des pages HTML dynamiques ou générer des emails.

Rendu HTML pour routes web

Dans OnServe(), ajoutez une route qui rend un template :

import (
    "html/template"
    "path/filepath"
    "github.com/pocketbase/pocketbase/core"
)

app.OnServe().BindFunc(func(e *core.ServeEvent) error {
    // Chargez les templates depuis pb_templates
    tpls := template.Must(template.ParseGlob("pb_templates/**/*.html"))

    e.Router.GET("/dashboard", func(re *core.RequestEvent) error {
        data := map[string]any{
            "User": re.AuthRecord, // Record auth si connecté
            "Title": "Mon Dashboard",
        }
        return core.RenderHtml(tpls, "dashboard.html", data, re)
    })

    return e.Next()
})

Créez pb_templates/dashboard.html :

<!DOCTYPE html>
<html>
<head><title>{{.Title}}</title></head>
<body>
    <h1>Bienvenue, {{if .User}}{{.User.Email}}{{else}}Visiteur{{end}}!</h1>
    {{template "partials/footer.html" .}}
</body>
</html>

Pour les layouts et partials : Utilisez {{define "layout"}} et {{template "layout" .}}. PocketBase fournit un helper core.RenderHtml qui gère le contexte et les erreurs.

Intégration avec fichiers statiques

Servez CSS/JS depuis pb_public (comme dans l'exemple initial). Pour un SPA avec SSR, rendez le template principal qui inclut votre bundle JS.

Templates pour Emails

Pour les emails, utilisez les mêmes templates avec app.NewMailClient().Send().

app.OnRecordAfterCreateSuccess("users").BindFunc(func(e *core.RecordEvent) error {
    client := e.App.NewMailClient()
    client.To = e.Record.GetString("email")
    client.Subject = "Bienvenue !"

    // Rendez un template email
    tpls := template.Must(template.ParseGlob("pb_templates/emails/*.html"))
    body, err := core.RenderHtml(tpls, "welcome.html", map[string]any{"User": e.Record}, nil)
    if err != nil {
        return err
    }
    client.HTMLBody = body

    return client.Send()
})

Exemple pb_templates/emails/welcome.html :

<h1>Bienvenue sur notre app !</h1>
<p>Bonjour {{.User.Email}}, merci de vous être inscrit.</p>

Personnalisez via hooks mailer comme OnMailerRecordVerificationSend() pour override le template par défaut.

Pour une startup, cela permet de créer un dashboard admin custom ou des newsletters sans framework externe comme Gin.

5. Build d'un exécutable unique

Le build statique est simple : go build -ldflags="-s -w" -o monapp. Cela strippe les symboles de debug et réduit la taille à ~7-11 MB.

Pour inclure des assets (templates, statiques) : Utilisez go:embed dans Go 1.23+.

import "embed"

//go:embed pb_public/* pb_templates/*
var embedFS embed.FS

// Dans OnServe()
se.Router.GET("/{path...}", apis.Static(&embedFS, false))

Rebuild et déployez : ./monapp serve --http=0.0.0.0:8090. Ajoutez un Dockerfile pour scaler :

FROM scratch
COPY monapp /
EXPOSE 8090
CMD ["/monapp", "serve"]

Conclusion

Avec ces extensions Go, PocketBase passe d'un backend basique à un framework full-stack adapté aux startups. Vous intégrez Stripe dans un hook after-create, testez via ApiScenario, et servez des pages HTML dynamiques – tout en un binaire portable. Commencez par l'exemple minimal, ajoutez un hook simple, et testez. Pour des cas plus complexes, consultez le repo GitHub de PocketBase.

Prochain guide : Extensions JS pour prototyper plus vite. Des questions ? Commentez ci-dessous.

Lire la suite