This guide provides an overview of the various ways to extend SnackBase functionality beyond the core features.
Overview
SnackBase is designed to be extensible at multiple levels, allowing you to add custom functionality without modifying core code.
Extension Philosophy
| Principle | Description |
|---|
| Composition over Modification | Add functionality through composition, not core changes |
| Stable APIs | Extension points have stable contracts |
| Pluggable | Features can be added/removed without affecting core |
| Backward Compatible | Extensions survive SnackBase upgrades |
What Can Be Extended?
| Area | Extension Method |
|---|
| Business Logic | Hooks, custom services |
| API Endpoints | New routers, middleware |
| Data Models | Custom tables, extended schemas |
| Authentication | Custom auth providers, MFA |
| Storage | Custom storage backends |
| Notifications | Custom notification channels |
| Validation | Custom validators, rules |
Extension Methods
1. Hooks (Recommended)
Best for: Business logic, event-driven automation, integrations
from src.snackbase.infrastructure.api.app import app
@app.hook.on_record_after_create("posts")
async def custom_logic(record: dict, context: Context):
"""Custom business logic after post creation."""
# Your code here
pass
Pros:
- Stable API (v1.0 contract)
- Automatic event triggering
- Account isolation built-in
- No core modifications
Cons:
- Limited to defined events
- Can’t add new API endpoints
2. Custom API Endpoints
Best for: New features, external integrations, custom operations
from fastapi import APIRouter
from src.snackbase.infrastructure.api.app import app
custom_router = APIRouter(prefix="/custom", tags=["custom"])
@custom_router.get("/analytics")
async def get_analytics(context: Context = Depends(get_context)):
"""Custom analytics endpoint."""
return {"analytics": "data"}
# Register in app.py
app.include_router(custom_router, prefix=API_PREFIX)
Pros:
- Full control over endpoint
- Can access all SnackBase services
- Leverage existing authentication
Cons:
- Must manually handle permissions
- Requires more code
3. Custom Database Tables
Best for: Domain-specific data, complex relationships
# Create migration
# alembic/versions/xxx_add_custom_table.py
def upgrade():
op.create_table(
"custom_features",
sa.Column("id", sa.String(50), primary_key=True),
sa.Column("account_id", sa.String(10), nullable=False, index=True),
sa.Column("name", sa.String(255), nullable=False),
sa.Column("config", sa.JSON, nullable=True),
sa.ForeignKeyConstraint(["account_id"], ["accounts.id"])
)
Pros:
- Full SQL control
- Complex relationships
- Migrations versioned
Cons:
- Requires manual migrations
- Not auto-integrated with collections
4. Middleware
Best for: Request/response processing, logging, custom auth
from src.snackbase.infrastructure.api.app import app
@app.middleware("http")
async def custom_middleware(request: Request, call_next):
"""Custom middleware for all requests."""
# Pre-processing
start_time = time.time()
# Call next middleware/route
response = await call_next(request)
# Post-processing
duration = time.time() - start_time
response.headers["X-Process-Time"] = str(duration)
return response
Pros:
- Runs on every request
- Can modify requests/responses
- Good for cross-cutting concerns
Cons:
- Adds latency to all requests
- Must be carefully designed
5. Custom Services
Best for: Reusable business logic, external integrations
# src/snackbase/infrastructure/services/custom_service.py
from typing import Any
class CustomAnalyticsService:
"""Custom analytics service."""
def __init__(self, db: AsyncSession):
self._db = db
async def calculate_metrics(self, account_id: str) -> dict[str, Any]:
"""Calculate custom metrics for account."""
# Your logic here
return {"metrics": "data"}
# Use in routes
@router.get("/analytics")
async def get_analytics(
context: Context = Depends(get_context),
db: AsyncSession = Depends(get_db)
):
service = CustomAnalyticsService(db)
return await service.calculate_metrics(context.account_id)
Pros:
- Encapsulates logic
- Reusable across endpoints
- Testable in isolation
Cons:
Choosing the Right Approach
Decision Tree
What do you want to do?
|
+-- Add business logic to existing operations
| +--> Use Hooks
|
+-- Create new API endpoints
| +--> Create Custom Router
|
+-- Store custom data structures
| |
| +-- Simple, per-record metadata
| | +--> Use JSON field in existing collection
| |
| +-- Complex, relational data
| +--> Create Custom Table
|
+-- Modify all requests/responses
| +--> Use Middleware
|
+-- Integrate external services
+--> Create Custom Service
Comparison Matrix
| Method | Use Case | Complexity | Survives Updates | Example |
|---|
| Hooks | Event-driven logic | Low | Yes | Send notification on create |
| Custom Router | New endpoints | Medium | Yes | Custom analytics API |
| Custom Table | Complex data | High | Yes | Audit log storage |
| Middleware | Request/response | Medium | Maybe | Custom logging |
| Core Modification | Framework changes | Very High | No | Changing auth flow |
Modifying core files will cause conflicts when updating SnackBase. Always use extension methods instead.
Extension Points
1. Database Layer
Extend the data layer:
# Custom repository
class CustomRepository:
def __init__(self, session: AsyncSession):
self._session = session
async def custom_query(self, account_id: str) -> list[dict]:
"""Custom database query."""
result = await self._session.execute(
select(CustomTable).where(
CustomTable.account_id == account_id
)
)
return [row.__dict__ for row in result.scalars()]
2. Service Layer
Add business logic services:
# src/snackbase/domain/services/analytics_service.py
class AnalyticsService:
"""Analytics business logic."""
async def generate_report(
self,
account_id: str,
date_range: DateRange
) -> Report:
"""Generate analytics report."""
# Business logic here
pass
3. API Layer
Extend the API:
# Custom router with authentication
from src.snackbase.infrastructure.api.dependencies import (
get_context,
require_permission
)
router = APIRouter(prefix="/analytics", tags=["analytics"])
@router.get("/report")
async def get_report(
context: Context = Depends(get_context),
authorized: bool = Depends(require_permission("analytics", "read"))
):
"""Get analytics report (requires permission)."""
# Your logic
pass
4. Authentication Layer
Extend authentication:
# Custom auth provider
class CustomAuthProvider:
async def authenticate(
self,
credentials: dict
) -> AuthResult:
"""Custom authentication logic."""
# Integrate with external auth provider
pass
# Register in auth service
auth_service.register_provider("custom", CustomAuthProvider())
Architecture Considerations
Clean Architecture Principles
When extending SnackBase, follow Clean Architecture:
+-----------------------------------------------------+
| API Layer |
| (Routers, Controllers, Middleware) |
+---------------------+-------------------------------+
|
+---------------------+-------------------------------+
| Application Layer |
| (Use Cases, Orchestration, Services) |
+---------------------+-------------------------------+
|
+---------------------+-------------------------------+
| Domain Layer |
| (Entities, Business Logic, Interfaces) |
+---------------------+-------------------------------+
|
+---------------------+-------------------------------+
| Infrastructure Layer |
| (Database, External APIs, Storage) |
+-----------------------------------------------------+
Dependency Direction
CORRECT: Dependencies point inward
Router -> Service -> Repository -> Database
INCORRECT: Dependencies point outward
Repository -> Service -> Router
Separation of Concerns
Keep extensions organized:
src/snackbase/
├── extensions/ # Your extensions
│ ├── analytics/ # Analytics feature
│ │ ├── router.py # API endpoints
│ │ ├── service.py # Business logic
│ │ └── repository.py # Data access
│ └── integrations/ # External integrations
│ └── slack.py # Slack integration
Deployment Considerations
Surviving Updates
| Extension Method | Survives SnackBase Updates |
|---|
| Hooks | Yes (stable API) |
| Custom Routers | Yes (separate files) |
| Custom Tables | Yes (separate migrations) |
| Middleware | Maybe (if core changes) |
| Core Modifications | No (will conflict) |
Extension Isolation
Keep extensions isolated to avoid conflicts:
# BAD: Modifying core files
# src/snackbase/infrastructure/api/routes/users_router.py
# (Adding custom logic here will conflict with updates)
# GOOD: Separate extension file
# src/snackbase/extensions/custom_users.py
# (Separate file survives updates)
Configuration
Use configuration for extension behavior:
# .env
ENABLE_ANALYTICS_FEATURE=true
SLACK_WEBHOOK_URL=https://hooks.slack.com/...
CUSTOM_API_KEY=your-key-here
# config.py
from pydantic import Settings
class ExtensionSettings(BaseSettings):
enable_analytics: bool = False
slack_webhook_url: str | None = None
custom_api_key: str | None = None
Examples
Example 1: Analytics Dashboard
Add custom analytics:
# 1. Create custom table (migration)
op.create_table(
"page_views",
sa.Column("id", sa.String(50), primary_key=True),
sa.Column("account_id", sa.String(10)),
sa.Column("path", sa.String(255)),
sa.Column("views", sa.Integer),
sa.Column("date", sa.Date)
)
# 2. Create service
class AnalyticsService:
async def get_page_views(self, account_id: str, days: int = 30):
"""Get page views for last N days."""
# Query and aggregate
pass
# 3. Create router
@router.get("/analytics/page-views")
async def page_views(
days: int = 30,
context: Context = Depends(get_context)
):
service = AnalyticsService(db)
return await service.get_page_views(context.account_id, days)
Example 2: Slack Integration
Add Slack notifications:
# 1. Create hook
@app.hook.on_record_after_create("posts")
async def notify_slack(record: dict, context: Context):
"""Send Slack notification on post creation."""
if record.get("status") == "published":
await slack_service.send_notification(
webhook_url=settings.slack_webhook_url,
message=f"New post: {record['title']}"
)
# 2. Create service
class SlackService:
async def send_notification(self, webhook_url: str, message: str):
"""Send notification to Slack."""
async with httpx.AsyncClient() as client:
await client.post(webhook_url, json={"text": message})
Example 3: Custom Validation
Add field validation:
# 1. Create hook
@app.hook.on_record_before_create("posts")
async def validate_post_content(record: dict, context: Context):
"""Validate post content before creation."""
content = record.get("content", "")
# Custom validation
if len(content) < 50:
raise HookAbortException(
message="Content must be at least 50 characters",
status_code=400
)
# Check for prohibited words
prohibited = ["spam", "advertisement"]
if any(word in content.lower() for word in prohibited):
raise HookAbortException(
message="Content contains prohibited words",
status_code=400
)
Example 4: Custom Endpoint with Permissions
Add protected endpoint:
# Custom router with permissions
router = APIRouter(prefix="/reports", tags=["reports"])
@router.get("/sales")
async def sales_report(
context: Context = Depends(get_context),
authorized: bool = Depends(require_permission("reports", "read"))
):
"""Generate sales report (requires reports:read permission)."""
# Generate report
report = await report_service.generate_sales_report(context.account_id)
return report
# Register permission
# Via UI or API:
# {
# "role": "manager",
# "collection": "reports",
# "read": true,
# "create": false,
# "update": false,
# "delete": false
# }
Summary
| Concept | Key Takeaway |
|---|
| Extension Methods | Hooks, routers, tables, middleware, services |
| Choosing Approach | Decision tree based on requirements |
| Clean Architecture | Follow layering, dependency direction |
| Deployment | Keep extensions isolated for updates |
| Best Practices | Don’t modify core, use configuration, test thoroughly |