Skip to main content
Version: 1.0 (Stable API) Status: Production Ready

Overview

The SnackBase Hook System is an extensibility framework that allows developers to inject custom logic at specific points in the application lifecycle. It provides a stable, event-driven API for extending SnackBase without modifying core code.

Key Features

  • Event-Driven Architecture: Subscribe to lifecycle events
  • Priority-Based Execution: Control hook execution order
  • Tag-Based Filtering: Target specific collections or resources
  • Before/After Hooks: Modify data or react to changes
  • Built-in Hooks: Core functionality (timestamps, account isolation)
  • Abort Capability: Cancel operations from before hooks
  • Async Support: Full async/await support
  • Stable API: Guaranteed backward compatibility
The Hook System API is stable and follows semantic versioning. Breaking changes will only occur in major version releases.

Stable API Contract

Guaranteed Stability

Stable (will not change):
  • HookRegistry.register() method signature
  • HookRegistry.trigger() method signature
  • HookRegistry.unregister() method signature
  • HookContext dataclass structure
  • AbortHookException behavior
  • Hook event naming convention
  • Priority-based execution order
  • Tag-based filtering mechanism
  • Built-in hook behavior
Additive Changes (non-breaking):
  • New hook events
  • New hook categories
  • New HookContext fields (optional)
  • New built-in hooks
  • New utility functions

Hook Categories

Hooks are organized into 8 categories:
CategoryDescriptionExamples
App LifecycleApplication startup/shutdownon_bootstrap, on_serve, on_terminate
Model OperationsInternal SQLAlchemy modelson_model_before_create, on_model_after_update
Record OperationsDynamic collection recordson_record_before_create, on_record_after_delete
Collection OperationsSchema changeson_collection_before_create, on_collection_after_update
Auth OperationsAuthentication eventson_auth_after_login, on_auth_before_register
Request ProcessingHTTP request lifecycleon_before_request, on_after_request
RealtimeWebSocket/SSE eventson_realtime_connect, on_realtime_message
MailerEmail sendingon_mailer_before_send, on_mailer_after_send

Hook Events

Naming Convention

All hook events follow a consistent pattern:
on_<category>_<timing>_<operation>

Examples:
- on_record_before_create
- on_auth_after_login
- on_collection_before_delete
Timing:
  • before_*: Called before operation (can modify data or abort)
  • after_*: Called after successful operation (read-only, side effects)

Complete Event List

Record Operations (Dynamic Collections)

EventTimingCan ModifyCan Abort
on_record_before_createBeforeYesYes
on_record_after_createAfterNoNo
on_record_before_updateBeforeYesYes
on_record_after_updateAfterNoNo
on_record_before_deleteBeforeNoYes
on_record_after_deleteAfterNoNo
on_record_before_queryBeforeYesYes
on_record_after_queryAfterYesNo

Auth Operations

EventTimingCan ModifyCan Abort
on_auth_before_loginBeforeYesYes
on_auth_after_loginAfterNoNo
on_auth_before_registerBeforeYesYes
on_auth_after_registerAfterNoNo
on_auth_before_logoutBeforeNoYes
on_auth_after_logoutAfterNoNo
on_auth_before_password_resetBeforeYesYes
on_auth_after_password_resetAfterNoNo

Collection Operations

EventTimingCan ModifyCan Abort
on_collection_before_createBeforeYesYes
on_collection_after_createAfterNoNo
on_collection_before_updateBeforeYesYes
on_collection_after_updateAfterNoNo
on_collection_before_deleteBeforeNoYes
on_collection_after_deleteAfterNoNo

Usage Guide

from snackbase.core.hooks import HookEvent

@app.state.hook.on_record_before_create("posts", priority=100)
async def validate_post(event, data, context):
    """Validate post before creation."""
    if data and len(data.get("title", "")) < 5:
        from snackbase.domain.entities.hook_context import AbortHookException
        raise AbortHookException("Title must be at least 5 characters")
    return data

Available Decorator Methods

Record Operations:
  • on_record_before_create(collection, priority=0)
  • on_record_after_create(collection, priority=0)
  • on_record_before_update(collection, priority=0)
  • on_record_after_update(collection, priority=0)
  • on_record_before_delete(collection, priority=0)
  • on_record_after_delete(collection, priority=0)
  • on_record_before_query(collection, priority=0)
  • on_record_after_query(collection, priority=0)
Collection Operations:
  • on_collection_before_create(priority=0)
  • on_collection_after_create(priority=0)
  • on_collection_before_update(priority=0)
  • on_collection_after_update(priority=0)
  • on_collection_before_delete(priority=0)
  • on_collection_after_delete(priority=0)
Auth Operations:
  • on_auth_before_login(priority=0)
  • on_auth_after_login(priority=0)
  • on_auth_before_register(priority=0)
  • on_auth_after_register(priority=0)

HookContext Structure

@dataclass
class HookContext:
    """Context passed to hooks."""
    app: Any                      # FastAPI app instance
    user: Optional["User"]        # Current authenticated user
    account_id: Optional[str]     # Current account ID
    request_id: str               # Request correlation ID
    request: Optional["Request"]  # FastAPI/Starlette Request object
    ip_address: Optional[str]     # Client IP address
    user_agent: Optional[str]     # Client user agent
    user_name: Optional[str]      # User display name

Aborting Operations

Use AbortHookException to cancel an operation from a before_* hook:
from snackbase.domain.entities.hook_context import AbortHookException

@app.state.hook.on_record_before_create("posts")
async def validate_post(event, data, context):
    """Prevent spam posts."""
    if data and "spam" in data.get("content", "").lower():
        raise AbortHookException("Spam content detected")
    return data

Built-in Hooks

SnackBase includes 4 built-in hooks that provide core functionality. These hooks are always active and cannot be disabled.

1. Timestamp Hook

Purpose: Automatically set created_at and updated_at timestamps. Events: on_record_before_create, on_record_before_update Priority: -100 (runs early)

2. Account Isolation Hook

Purpose: Enforce multi-tenancy by setting account_id from context. Events: on_record_before_create Priority: -200 (runs very early)

3. Created By Hook

Purpose: Track which user created/updated records. Events: on_record_before_create, on_record_before_update Priority: -150

4. Audit Capture Hook

Purpose: Automatically capture audit log entries for record operations. Events: on_record_after_create, on_record_after_update, on_record_after_delete Priority: 100 (runs after all user hooks)

Built-in Hook Execution Order

Priority -200: account_isolation_hook  (set account_id)

Priority -150: created_by_hook         (set created_by/updated_by)

Priority -100: timestamp_hook          (set timestamps)

Priority 0+:   User hooks              (custom logic)

Priority 100:  audit_capture_hook      (capture audit logs)

Creating Custom Hooks

Example 1: Data Validation

from snackbase.domain.entities.hook_context import AbortHookException

@app.state.hook.on_record_before_create("products", priority=50)
async def validate_product_price(event, data, context):
    """Ensure product price is positive."""
    if data and data.get("price", 0) <= 0:
        raise AbortHookException("Price must be greater than 0")
    return data

Example 2: Data Transformation

@app.state.hook.on_record_before_create("users", priority=50)
async def normalize_email(event, data, context):
    """Normalize email to lowercase."""
    if data and "email" in data:
        data["email"] = data["email"].lower().strip()
    return data

Example 3: Computed Fields

@app.state.hook.on_record_before_create("orders", priority=50)
async def calculate_total(event, data, context):
    """Calculate order total from line items."""
    if data and "items" in data:
        total = sum(item["price"] * item["quantity"] for item in data["items"])
        data["total"] = total
    return data

Example 4: Notifications

@app.state.hook.on_record_after_create("orders", priority=50)
async def send_order_notification(event, data, context):
    """Send email notification when order is created."""
    if data:
        await send_email(
            to=context.user.email,
            subject="Order Confirmation",
            body=f"Your order {data['id']} has been created."
        )
    return data

Advanced Features

Priority-Based Execution

Hooks execute in priority order (higher priority = earlier execution):
# Priority 200 - runs first
@app.state.hook.on_record_before_create("posts", priority=200)
async def hook_1(event, data, context):
    data["step"] = "1"
    return data

# Priority 100 - runs second
@app.state.hook.on_record_before_create("posts", priority=100)
async def hook_2(event, data, context):
    data["step"] = "2"
    return data

# Priority 0 (default) - runs last
@app.state.hook.on_record_before_create("posts")
async def hook_3(event, data, context):
    data["step"] = "3"
    return data

Tag-Based Filtering

Target specific collections or resources:
# Only for 'posts' collection
registry.register(
    event=HookEvent.ON_RECORD_BEFORE_CREATE,
    callback=my_hook,
    filters={"collection": "posts"}
)

Error Handling

Hooks can fail without crashing the system:
# Default: errors are logged but don't stop execution
registry.register(
    event=HookEvent.ON_RECORD_AFTER_CREATE,
    callback=my_hook,
    stop_on_error=False  # Default
)

Best Practices

1. Keep Hooks Focused

# Bad - does too much
async def mega_hook(event, data, context):
    validate_data(data)
    send_email(data)
    update_cache(data)
    return data

# Good - single responsibility
async def validate_data_hook(event, data, context):
    validate_data(data)
    return data

2. Use Appropriate Priorities

# Validation: High priority (runs early)
@app.state.hook.on_record_before_create("posts", priority=100)
async def validate(event, data, context):
    pass

# Enrichment: Medium priority
@app.state.hook.on_record_before_create("posts", priority=50)
async def enrich(event, data, context):
    pass

# Side effects: Low priority (runs late)
@app.state.hook.on_record_after_create("posts", priority=10)
async def notify(event, data, context):
    pass

3. Handle Errors Gracefully

@app.state.hook.on_record_after_create("posts")
async def send_notification(event, data, context):
    """Send notification, but don't fail if it errors."""
    try:
        await send_email(data)
    except Exception as e:
        logger.error("Failed to send notification", error=str(e))
    return data

API Reference

HookRegistry

class HookRegistry:
    def register(
        self,
        event: str,
        callback: Callable,
        filters: Optional[dict[str, Any]] = None,
        priority: int = 0,
        stop_on_error: bool = False,
        is_builtin: bool = False,
    ) -> str:
        """Register a hook."""

    async def trigger(
        self,
        event: str,
        data: Optional[dict[str, Any]] = None,
        context: Optional[HookContext] = None,
        filters: Optional[dict[str, Any]] = None,
    ) -> dict[str, Any]:
        """Execute all hooks for an event."""

    def unregister(self, hook_id: str) -> bool:
        """Remove a registered hook."""

AbortHookException

class AbortHookException(Exception):
    """Raise to abort an operation from a before hook."""
    def __init__(
        self,
        message: str,
        status_code: int = 400
    ):
        super().__init__(message)
        self.status_code = status_code