Extensions
Extend Solobase with powerful compile-time extensions
Extensions
Solobase provides a powerful, compile-time extension system that allows developers to extend the platform’s functionality while maintaining security and isolation. Extensions are compiled into your application, ensuring optimal performance while maintaining isolation through schema separation and security boundaries.
Overview
The extension system enables you to:
- Add custom API endpoints and functionality
- Extend the admin dashboard with new interfaces
- Hook into existing functionality
- Manage database migrations per extension
- Implement custom middleware
- Create reusable components
Quick Start
Using Official Extensions
package main
import (
"github.com/suppers-ai/solobase"
"github.com/suppers-ai/solobase/extensions/official/webhooks"
"github.com/suppers-ai/solobase/extensions/official/analytics"
)
func main() {
app := solobase.New()
// Register official extensions
app.RegisterExtension(webhooks.New())
app.RegisterExtension(analytics.New())
app.Initialize()
app.Start()
}
Creating Your Own Extension
Basic Extension Structure
Create a new extension by implementing the Extension interface:
package myextension
import (
"context"
"github.com/suppers-ai/solobase/extensions/core"
)
type MyExtension struct {
services *core.ExtensionServices
enabled bool
}
func New() *MyExtension {
return &MyExtension{
enabled: true,
}
}
// Metadata returns information about the extension
func (e *MyExtension) Metadata() core.ExtensionMetadata {
return core.ExtensionMetadata{
Name: "my-extension",
Version: "1.0.0",
Description: "My custom extension",
Author: "Your Name",
License: "MIT",
}
}
// Initialize sets up the extension with core services
func (e *MyExtension) Initialize(ctx context.Context, services *core.ExtensionServices) error {
e.services = services
services.Logger().Info(ctx, "Extension initializing")
return nil
}
// Start begins the extension's operation
func (e *MyExtension) Start(ctx context.Context) error {
e.services.Logger().Info(ctx, "Extension started")
return nil
}
// Stop gracefully shuts down the extension
func (e *MyExtension) Stop(ctx context.Context) error {
e.enabled = false
return nil
}
// Health returns the extension's health status
func (e *MyExtension) Health(ctx context.Context) (*core.HealthStatus, error) {
return &core.HealthStatus{
Status: "healthy",
Message: "Extension is running",
}, nil
}
Adding Routes
Extensions can register their own API routes:
func (e *MyExtension) RegisterRoutes(router core.ExtensionRouter) error {
// Register API endpoints
router.HandleFunc("/api/my-extension/data", e.handleData).Methods("GET")
router.HandleFunc("/api/my-extension/process", e.handleProcess).Methods("POST")
// Register admin dashboard routes
router.HandleFunc("/admin/my-extension", e.handleDashboard).Methods("GET")
return nil
}
func (e *MyExtension) handleData(w http.ResponseWriter, r *http.Request) {
// Handle data request
data := map[string]interface{}{
"message": "Hello from extension",
"enabled": e.enabled,
}
json.NewEncoder(w).Encode(data)
}
func (e *MyExtension) handleProcess(w http.ResponseWriter, r *http.Request) {
// Process incoming data
var input map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Process the input...
result := processData(input)
json.NewEncoder(w).Encode(result)
}
Database Migrations
Extensions can manage their own database schema and migrations:
func (e *MyExtension) DatabaseSchema() string {
return "ext_myextension"
}
func (e *MyExtension) Migrations() []core.Migration {
return []core.Migration{
{
Version: "001",
Description: "Create initial tables",
Extension: "my-extension",
Up: `
CREATE SCHEMA IF NOT EXISTS ext_myextension;
CREATE TABLE ext_myextension.settings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
key VARCHAR(255) UNIQUE NOT NULL,
value JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE ext_myextension.events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type VARCHAR(100) NOT NULL,
payload JSONB,
processed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_events_type ON ext_myextension.events(type);
CREATE INDEX idx_events_processed ON ext_myextension.events(processed);
`,
Down: `DROP SCHEMA IF EXISTS ext_myextension CASCADE;`,
},
{
Version: "002",
Description: "Add user tracking",
Extension: "my-extension",
Up: `
ALTER TABLE ext_myextension.events
ADD COLUMN user_id UUID,
ADD COLUMN metadata JSONB;
`,
Down: `
ALTER TABLE ext_myextension.events
DROP COLUMN user_id,
DROP COLUMN metadata;
`,
},
}
}
Using Hooks
Extensions can hook into various system events:
func (e *MyExtension) RegisterHooks() []core.HookRegistration {
return []core.HookRegistration{
{
Extension: "my-extension",
Name: "before-user-create",
Type: core.HookPreRequest,
Priority: 10,
Handler: e.beforeUserCreate,
},
{
Extension: "my-extension",
Name: "after-file-upload",
Type: core.HookPostRequest,
Priority: 20,
Handler: e.afterFileUpload,
},
{
Extension: "my-extension",
Name: "on-auth",
Type: core.HookAuthentication,
Priority: 5,
Handler: e.onAuthentication,
},
}
}
func (e *MyExtension) beforeUserCreate(ctx context.Context, hctx *core.HookContext) error {
// Validate or modify user data before creation
if userData, ok := hctx.Data["user"].(map[string]interface{}); ok {
// Add custom validation
if email, ok := userData["email"].(string); ok {
if !isValidEmail(email) {
return fmt.Errorf("invalid email format")
}
}
// Add default metadata
userData["metadata"] = map[string]interface{}{
"source": "my-extension",
"timestamp": time.Now(),
}
}
return nil
}
func (e *MyExtension) afterFileUpload(ctx context.Context, hctx *core.HookContext) error {
// Process uploaded files
if file, ok := hctx.Data["file"].(map[string]interface{}); ok {
// Log file upload
e.services.Logger().Info(ctx, "File uploaded",
"filename", file["name"],
"size", file["size"])
// Trigger processing if needed
go e.processUploadedFile(ctx, file)
}
return nil
}
Configuration Management
Extensions can have their own configuration:
func (e *MyExtension) ConfigSchema() json.RawMessage {
schema := map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"enabled": map[string]interface{}{
"type": "boolean",
"description": "Enable or disable the extension",
"default": true,
},
"apiKey": map[string]interface{}{
"type": "string",
"description": "API key for external service integration",
},
"webhookUrl": map[string]interface{}{
"type": "string",
"format": "uri",
"description": "Webhook URL for notifications",
},
"maxRetries": map[string]interface{}{
"type": "integer",
"minimum": 1,
"maximum": 10,
"default": 3,
"description": "Maximum number of retry attempts",
},
},
"required": []string{"enabled"},
}
data, _ := json.Marshal(schema)
return data
}
func (e *MyExtension) ValidateConfig(config json.RawMessage) error {
var cfg map[string]interface{}
if err := json.Unmarshal(config, &cfg); err != nil {
return fmt.Errorf("invalid config format: %w", err)
}
// Custom validation logic
if apiKey, ok := cfg["apiKey"].(string); ok {
if len(apiKey) < 32 {
return fmt.Errorf("API key must be at least 32 characters")
}
}
return nil
}
func (e *MyExtension) ApplyConfig(config json.RawMessage) error {
var cfg struct {
Enabled bool `json:"enabled"`
APIKey string `json:"apiKey"`
WebhookURL string `json:"webhookUrl"`
MaxRetries int `json:"maxRetries"`
}
if err := json.Unmarshal(config, &cfg); err != nil {
return err
}
e.enabled = cfg.Enabled
// Apply other configuration...
return nil
}
Extension Services
Extensions have access to a comprehensive set of services:
Database Service
// Schema-isolated database access
db := e.services.DB()
// All queries are automatically scoped to the extension's schema
rows, err := db.Query(ctx, "SELECT * FROM settings WHERE key = $1", "my-key")
// Transaction support
tx, err := db.Begin(ctx)
defer tx.Rollback(ctx)
_, err = tx.Exec(ctx, "INSERT INTO events (type, payload) VALUES ($1, $2)",
"user.created", payload)
err = tx.Commit(ctx)
Authentication Service
auth := e.services.Auth()
// Verify user permissions
hasPermission := auth.HasPermission(ctx, userID, "myext.manage")
// Get current user
user, err := auth.GetCurrentUser(ctx, r)
// Create API tokens
token, err := auth.CreateToken(ctx, userID, []string{"read", "write"})
Storage Service
storage := e.services.Storage()
// Upload file
fileID, err := storage.Upload(ctx, "path/to/file.pdf", fileData)
// Download file
data, err := storage.Download(ctx, fileID)
// Delete file
err = storage.Delete(ctx, fileID)
// List files
files, err := storage.List(ctx, "path/to/")
Logger Service
logger := e.services.Logger()
// Structured logging
logger.Info(ctx, "Processing request",
"user_id", userID,
"action", "create",
"resource", "document")
logger.Error(ctx, "Failed to process",
"error", err,
"retry_count", retries)
Security
Permission System
Extensions must declare required permissions:
func (e *MyExtension) RequiredPermissions() []core.Permission {
return []core.Permission{
{
Name: "myext.read",
Description: "Read extension data",
Resource: "myext_data",
Actions: []string{"read"},
},
{
Name: "myext.manage",
Description: "Manage extension settings",
Resource: "myext_settings",
Actions: []string{"create", "read", "update", "delete"},
},
}
}
Resource Quotas
Extensions are subject to configurable resource quotas:
- Maximum memory usage
- Maximum goroutines
- Maximum database connections
- Maximum storage space
- Request rate limiting
Schema Isolation
Each extension operates in its own database schema, preventing unauthorized access to other data:
// This query is automatically scoped to ext_myextension schema
db.Query(ctx, "SELECT * FROM settings")
// Executes: SELECT * FROM ext_myextension.settings
Middleware
Extensions can add custom middleware:
func (e *MyExtension) RegisterMiddleware() []core.MiddlewareRegistration {
return []core.MiddlewareRegistration{
{
Name: "rate-limiter",
Priority: 10,
Handler: e.rateLimitMiddleware,
},
{
Name: "auth-validator",
Priority: 20,
Handler: e.authValidatorMiddleware,
},
}
}
func (e *MyExtension) rateLimitMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Implement rate limiting logic
if !e.checkRateLimit(r) {
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
Testing Extensions
Unit Testing
func TestMyExtension(t *testing.T) {
// Create test suite
suite := core.NewExtensionTestSuite(t)
defer suite.Cleanup()
// Create and register extension
ext := New()
err := suite.Registry.Register(ext)
assert.NoError(t, err)
// Test initialization
err = suite.Registry.Enable("my-extension")
assert.NoError(t, err)
// Test API endpoint
resp := suite.TestRequest("GET", "/api/my-extension/data", nil)
assert.Equal(t, http.StatusOK, resp.Code)
var data map[string]interface{}
err = json.Unmarshal(resp.Body.Bytes(), &data)
assert.NoError(t, err)
assert.Equal(t, "Hello from extension", data["message"])
}
Integration Testing
func TestExtensionIntegration(t *testing.T) {
suite := core.NewExtensionTestSuite(t)
defer suite.Cleanup()
ext := New()
suite.LoadExtension(ext)
// Test with real database
ctx := context.Background()
db := suite.Services.DB()
// Insert test data
_, err := db.Exec(ctx,
"INSERT INTO settings (key, value) VALUES ($1, $2)",
"test-key", map[string]interface{}{"data": "test"})
assert.NoError(t, err)
// Query data
var value map[string]interface{}
err = db.QueryRow(ctx,
"SELECT value FROM settings WHERE key = $1",
"test-key").Scan(&value)
assert.NoError(t, err)
assert.Equal(t, "test", value["data"])
}
Official Extensions
Webhooks Extension
Provides webhook management and delivery:
import "github.com/suppers-ai/solobase/extensions/official/webhooks"
app.RegisterExtension(webhooks.New())
Features:
- Webhook endpoint management
- Automatic retry with exponential backoff
- HMAC signature verification
- Delivery history and logs
Analytics Extension
Tracks and analyzes application usage:
import "github.com/suppers-ai/solobase/extensions/official/analytics"
app.RegisterExtension(analytics.New())
Features:
- Event tracking
- User behavior analytics
- Performance metrics
- Custom dashboards
Storage Extension
Advanced file storage capabilities:
import "github.com/suppers-ai/solobase/extensions/official/storage"
app.RegisterExtension(storage.New())
Features:
- Multi-provider support (S3, GCS, Azure)
- Image optimization
- File versioning
- Access control
Best Practices
1. Error Handling
Always return meaningful errors:
if err != nil {
return fmt.Errorf("failed to process data: %w", err)
}
2. Logging
Use structured logging for better debugging:
logger.Info(ctx, "Processing request",
"extension", "my-extension",
"action", action,
"user_id", userID,
"duration", time.Since(start))
3. Database Queries
Use parameterized queries to prevent SQL injection:
// Good
db.Query(ctx, "SELECT * FROM users WHERE id = $1", userID)
// Bad - never do this!
db.Query(ctx, fmt.Sprintf("SELECT * FROM users WHERE id = %s", userID))
4. Resource Cleanup
Always clean up resources:
func (e *MyExtension) Stop(ctx context.Context) error {
// Close connections
if e.conn != nil {
e.conn.Close()
}
// Cancel background tasks
if e.cancel != nil {
e.cancel()
}
return nil
}
5. Testing
Write comprehensive tests:
// Test normal operation
func TestExtensionNormalOperation(t *testing.T) { ... }
// Test error cases
func TestExtensionErrorHandling(t *testing.T) { ... }
// Test configuration
func TestExtensionConfiguration(t *testing.T) { ... }
// Benchmark performance
func BenchmarkExtension(b *testing.B) { ... }
Deployment
Development
# Run with extensions in development
go run main.go
Production
# Build with extensions
go build -o myapp main.go
# Run in production
./myapp
Docker
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp main.go
FROM alpine:latest
COPY --from=builder /app/myapp /myapp
ENTRYPOINT ["/myapp"]
Troubleshooting
Extension Not Loading
Check:
- Extension is registered in main.go
- All required methods are implemented
- Check logs for initialization errors
Migration Failures
// Check migration status
status, err := e.services.DB().GetMigrationStatus(ctx)
// Rollback if needed
err = e.services.DB().RollbackMigration(ctx, "001")
Performance Issues
- Check metrics and logs
- Review database queries for N+1 problems
- Verify resource quotas aren’t being exceeded
Next Steps
- Browse Official Extensions
- Explore Community Extensions
- Read the Extension API Reference
- Join our Discord Community for help