15 KiB
AGENTS.md — InfraSynth Base
Project Overview
InfraSynth Base is a reusable Django backend infrastructure kit distributed as a single pip package (infrasynth-base). It provides 9 Django apps that cover authentication, authorization, audit logging, file storage, notifications, webhooks, workflows, job scheduling, feature flags, and billing. External systems (App B) install this package and build their domain apps on top without modifying InfraSynth source code.
One version, one repo, one pip install. Feature flags control what is active per tenant/user.
Stack
| Component | Technology |
|---|---|
| Language | Python 3.12+ |
| Framework | Django 5.2+ |
| API | Django REST Framework 3.16+ |
| Database | PostgreSQL 16 |
| Cache/Broker | Redis |
| Task Queue | Celery 5.4+ (with django-celery-results + django-celery-beat) |
| Auth | JWT via HTTP-Only cookies (encrypted with Fernet) + API Keys |
| File Storage | S3, Cloudinary, GCS, local (via django-storages) |
| Payments | Stripe, MercadoPago, Wompi |
| 2FA | TOTP (pyotp + qrcode) |
| Anti-spam | ALTCHA (proof-of-work, self-hosted) |
| Monitoring | Flower (Celery dashboard) |
| Tests | pytest + pytest-django + factory-boy |
| Linting | ruff + mypy + pre-commit |
Package Structure
infrasynth-base/
├── pyproject.toml # Root package metadata
├── docker-compose.yml
├── PLAN.md # Architecture blueprint
├── AGENTS.md # This file
│
├── infrasynth/ # Namespace package root
│ ├── shared/ # NOT a Django app. Zero-Django utilities.
│ ├── audit/ # Django app: 'infrasynth.audit'
│ ├── security/ # Django app: 'infrasynth.security'
│ ├── files/ # Django app: 'infrasynth.files'
│ ├── notifications/ # Django app: 'infrasynth.notifications'
│ ├── webhooks/ # Django app: 'infrasynth.webhooks'
│ ├── workflows/ # Django app: 'infrasynth.workflows'
│ ├── scheduler/ # Django app: 'infrasynth.scheduler'
│ ├── features/ # Django app: 'infrasynth.features'
│ └── billing/ # Django app: 'infrasynth.billing'
│
└── tests/
├── conftest.py
├── test_audit/
├── test_security/
├── test_files/
├── test_notifications/
├── test_webhooks/
├── test_workflows/
├── test_scheduler/
├── test_features/
└── test_billing/
Architecture Principles
1. Zero Cross-App Import Rule
No Django app that depends on another Django app may import from it directly. The only allowed intra-app imports are:
infrasynth.shared.*(protocols, enums, crypto, types)- Django stdlib (
django.db.models,django.conf.settings,django.dispatch.Signal)
2. Integration Mechanisms (in priority order)
| Mechanism | When to use | Example |
|---|---|---|
| Settings dict | Configure which concrete class/backend to use | INFRASYNTH_NOTIFICATIONS["CHANNELS"]["email"] points to SMTPChannel |
| Signals | Loose async communication between apps | billing emits payment_succeeded, App B's receiver sends email via notifications |
| Registries | Apps self-register capabilities at startup | EventRegistry.register("helpdesk.ticket.created") in apps.py:ready() |
| ABCs/Protocols | Define swappable interfaces | BasePaymentGateway, BaseChannel, DataValidatorProtocol |
| ForeignKey (SET_NULL) | Weak model coupling | StoredFile referenced by any model, on_delete=SET_NULL, related_name="+" |
| AUTH_USER_MODEL | Reference the user model | Always settings.AUTH_USER_MODEL, never auth.User directly |
| FeatureService | Cross-cutting enable/disable | FeatureService().is_enabled("billing") gates billing views |
3. Dependency Graph
infrasynth.shared ← Zero deps (protocols, enums, crypto)
↑
infrasynth.audit ← shared only
↑
All other Django apps ← shared + audit only
↑
infrasynth.features ← Used by ALL apps for feature gating
↑ (but apps register flags, don't import features)
4. Feature Flags Are the Orchestrator
infrasynth.features is the only app that is always active. Every other feature (module, endpoint, UI element) should be gated behind a feature flag. The frontend consumes GET /api/features/active/ once at boot and renders conditionally.
Each app registers its flags in apps.py:ready():
class MyAppConfig(AppConfig):
def ready(self):
from infrasynth.features.registry import FeatureRegistry
FeatureRegistry.register("my_app.feature_x", default=True)
Development Conventions
Django App Structure
Every Django app follows this layout:
app_name/
├── __init__.py
├── apps.py # AppConfig: name, feature_flag, ready() for registry registrations
├── models.py # Django models
├── services.py # Business logic (pure Python, no DRF)
├── urls.py # URL patterns
├── serializers.py # DRF serializers
├── views.py # DRF views
├── filters.py # DRF filtersets
├── signals.py # Signal definitions (Signal() instances)
├── middleware.py # Django middleware (if needed)
├── tasks.py # Celery tasks (if needed)
└── migrations/ # Django migrations
Model Conventions
- All models use
db_tableprefix:audit_model_change_log,security_api_key,files_stored_file, etc. - ForeignKey always uses
SET_NULLwithnull=True, blank=Trueunless cascade is semantically required. related_name="+"on FK to other apps' models to avoid reverse relation clutter.settings.AUTH_USER_MODELfor user references. Never hardcodeauth.User.- JSONField for flexible metadata, not TextField.
- Use
infrasynth.shared.enumsfor choice fields (never hardcode strings in choices).
Serializer Conventions
- FK fields need
{field}_inforead-only serialized representations (for frontend display). - Audit fields (
created_by,created_at,updated_by,updated_at) when present must be inread_only_fieldsand are populated by signals (not inModeloAuditablebase class since we avoid model inheritance). - JSONField fields need explicit serialization handling (the frontend expects objects, not strings).
- Use
SerializerMethodFieldsparingly — prefer annotations in the queryset.
View Conventions
- All views are
ModelViewSetunless they have no model backing. - Always set
permission_classes = [IsAuthenticated]plus specific permission classes. - Always use
select_related()/prefetch_related()inget_queryset()to avoid N+1 queries. - Feature flag check in
initial()method for gated views:
def initial(self, request, *args, **kwargs):
if not FeatureService().is_enabled("billing", user=request.user):
raise NotFound()
super().initial(request, *args, **kwargs)
- Pagination: All list views use the standard
CustomPaginationclass. Query param?page_size=(default 25, max 100). - Filtering: Use
DjangoFilterBackendwith aFilterSetclass per view.
Signal Conventions
- Define signals in
signals.pyas module-levelSignal()instances. - Receiver functions go in
receivers.pyorapps.py:ready()(for connecting signals across apps). - Always use
sender=parameter when connecting to specific model signals. - Use
@receiver(signal_name)decorator pattern.
Registry Conventions
Registries are singleton classes (not instances) with @classmethod methods. They live in a registry.py file in their owning app:
infrasynth.features.registry.FeatureRegistry— feature flag definitionsinfrasynth.webhooks.registry.EventRegistry— event definitionsinfrasynth.notifications.resolvers.VariableResolverRegistry— template variable resolversinfrasynth.workflows.validators.DataValidatorRegistry— workflow data validators
Pattern:
class MyRegistry:
_items: dict = {}
@classmethod
def register(cls, key, **kwargs):
cls._items[key] = kwargs
@classmethod
def get(cls, key):
return cls._items.get(key)
@classmethod
def get_all(cls):
return dict(cls._items)
Testing Conventions
-
Use pytest with
pytest-django(pytest.mark.django_db). -
Use factory-boy for model factories (
tests/factories.pyin each app test directory). -
API tests use
APIClientfrom DRF with JWT cookies set manually. -
Test structure:
test_models.py— model creation, validation, constraintstest_services.py— business logictest_views.py— API endpoints (auth, permissions, CRUD, edge cases)test_signals.py— signal emission and receiver behaviortest_integration.py— cross-app communication (registries, signals)
-
Conftest fixtures:
api_client— DRF APIClientauthenticated_client— APIClient with JWT cookies setadmin_client— authenticated superuser clientuser_factory,role_factory, etc.
Settings Conventions
- All InfraSynth settings use the prefix
INFRASYNTH_followed by the app name in uppercase. - Settings are dicts, not flat keys:
INFRASYNTH_SECURITY = {"COOKIE_SECURE": True}. - Every setting has a sensible default — the system must run with zero configuration in development.
- Read settings with the helper (not
getattrdirectly):
from infrasynth.shared.settings_utils import get_setting
cookie_secure = get_setting("INFRASYNTH_SECURITY", "COOKIE_SECURE", True)
Crypto Conventions
- Use
infrasynth.shared.cryptofor Fernet encryption/decryption. - Encrypt secrets at rest: API keys, SMTP passwords, payment gateway credentials.
- Never log encrypted values — log the fact of encryption, not the ciphertext or plaintext.
- CRYPTO_KEY must be set in environment. Auto-generate in dev if missing (warn loudly).
Migration Conventions
- Apps are namespaced in migrations to avoid collisions:
infrasynth.audit.migrationsinfrasynth.security.migrations
- Never use
RunPythonwith model imports — useapps.get_model(). - Data migrations go in separate migration files from schema migrations.
Integration Examples for External App B
App B needs: custom notification channel
# helpdesk/channels.py
from infrasynth.notifications.channels.base import BaseChannel
class SlackChannel(BaseChannel):
channel_type = "slack"
def send(self, recipient, subject, body, is_html=True, attachments=None):
# Send to Slack webhook
...
return Result.ok(True)
# settings.py
INFRASYNTH_NOTIFICATIONS = {
"CHANNELS": {
"slack": {
"primary": "helpdesk.channels.SlackChannel",
},
},
}
App B needs: webhook handler for a new external service
# helpdesk/webhook_handlers.py
from infrasynth.webhooks.inbound.handlers import BaseInboundHandler
class JiraWebhookHandler(BaseInboundHandler):
def verify(self, payload, headers, secret):
# Verify Jira HMAC
...
def process(self, event_type, payload):
# Sync Jira issue to local Ticket model
...
# Register via admin or data migration: InboundEndpoint(slug="jira", handler="helpdesk.webhook_handlers.JiraWebhookHandler")
App B needs: workflow data validation for its domain
# helpdesk/validators.py
class TicketDataValidator:
def validate(self, node, data, context):
if not data.get("resolution_note"):
raise ValidationError({"resolution_note": "Required when resolving."})
return data
# helpdesk/apps.py → ready():
DataValidatorRegistry.register("ticket_approval", TicketDataValidator())
Common Patterns and Anti-Patterns
✅ DO
- Use
settings.AUTH_USER_MODELfor all user references - Use signals for cross-app communication
- Register events/resolvers/validators in
apps.py:ready() - Gate views/endpoints behind feature flags
- Use
on_delete=SET_NULLwithnull=True, blank=Truefor cross-app FKs - Use
related_name="+"for FKs to models in other apps - Use
db_tableprefix for all models - Encrypt secrets at rest with Fernet
- Use
Result[T, E]monad for service methods that can fail - Add
select_related()/prefetch_related()in every view'sget_queryset()
❌ DON'T
- Don't import models from one Django app into another Django app
- Don't hardcode
auth.User— usesettings.AUTH_USER_MODEL - Don't use
on_delete=CASCADEon cross-app FKs - Don't bypass the FeatureRegistry — always register flags
- Don't hardcode channel URLs, gateway credentials, or SMTP settings in code
- Don't log plaintext secrets, tokens, or passwords
- Don't use signals for synchronous request-response flows (use direct method calls)
- Don't create circular imports — if app A needs app B, and app B needs app A, refactor into shared or use signals
- Don't store file contents in the database — always use the files app's storage abstraction
Key Files Reference
| File | Purpose |
|---|---|
infrasynth/shared/protocols.py |
All ABCs and Protocols |
infrasynth/shared/crypto.py |
Fernet encrypt/decrypt/rotation |
infrasynth/shared/enums.py |
All shared enums |
infrasynth/shared/results.py |
Result monad |
infrasynth/features/registry.py |
Feature flag registry |
infrasynth/features/services.py |
Feature flag evaluation |
infrasynth/webhooks/registry.py |
Event registry |
infrasynth/notifications/resolvers.py |
Template variable resolvers |
infrasynth/notifications/channels/base.py |
Channel ABC |
infrasynth/billing/gateways/base.py |
Payment gateway ABC |
infrasynth/workflows/validators.py |
Data validator protocol + registry |
infrasynth/workflows/models.py |
WorkflowAwareModel abstract mixin |
infrasynth/security/services.py |
AuthorizationService |
infrasynth/security/auth/cookies.py |
CookieJWTAuthentication |
infrasynth/security/auth/api_keys.py |
APIKeyAuthentication |
infrasynth/security/permissions.py |
HybridPermission + require_permission |
infrasynth/files/services.py |
FileService (upload, signed_url, delete) |
infrasynth/scheduler/services.py |
TaskService |
Setup for Development
# Clone
git clone <repo-url> && cd infrasynth-base
# Virtual environment
python -m venv .venv && source .venv/bin/activate
# Install with dev dependencies
pip install -e ".[dev]"
# Start services
docker compose up -d db redis
# Run migrations
python manage.py migrate
# Run tests
pytest
# Run linter
ruff check .
# Run type checker
mypy infrasynth/