Feature flags + per-app install: how to spin a client site from this template #2

Closed
opened 2026-08-13 04:25:52 -07:00 by westfarn · 1 comment
Owner

Why

This repo is the Gitea template for client websites (Web Design & Hosting catalog). monica_site is the first instantiation — it works, but every feature is always installed. Dashboard, URLs, and models import messaging and social unconditionally. Next client with a smaller package should not get Email/SMS, PCM, blog, Stripe, social, or Ollama drafts unless those features are purchased and turned on.

Goal: clone this template, set branding + secret feature flags, deploy. Disabled features are not in INSTALLED_APPS, so they have no URLs, models, admin, nav, or templates. Guessing /portal/social/ 404s. No flag-only hide in templates while the app still loads.

Related: #1 (populate the template from monica_site). Follow this ticket when extracting.


Catalog → Django apps

Always included (no flag; always in INSTALLED_APPS):

Catalog item Django app(s) Notes
Public site (landing, about, contact + service pages) public reCAPTCHA contact form
Client portal + UTM (login, dashboard, leads, UTM) accounts, dashboard, leads, analytics login / logout / session
Shared platform core, contacts contacts = people store for leads + optional channels

Optional add-ons — one Django app per catalog feature:

Catalog item App Secret flag Depends on
Email & SMS (SMTP2GO campaigns, mailing list, engagement) email_sms FEATURE_EMAIL_SMS
Direct mail (PCM postcard designer + print/send) directmail FEATURE_DIRECT_MAIL
Blog (public blog + portal post mgmt) blog FEATURE_BLOG
Payments (Stripe invoices + pay links) payments FEATURE_PAYMENTS FEATURE_EMAIL_SMS
Social consolidation (accounts, composer, scheduling) social FEATURE_SOCIAL
AI social generator (Ollama drafts) social_ai FEATURE_SOCIAL_AI FEATURE_SOCIAL

Do not keep postcard inside email_sms / messaging, and do not keep Ollama generate inside social. Those are separate SKUs.

monica_site today: messaging = email + SMS + PCM; social includes api/generate/ + social.ollama. Split when extracting.


Secret flags (source of truth)

Flags live in the deploy secret env files, same place as DJANGO_SECRET_KEY — never committed:

~/Documents/secrets/<client>_site/<client>_site_prod.env
~/Documents/secrets/<client>_site/<client>_site_beta.env

Template ships .env.example / .env.prod.example with the keys documented, values empty/false.

# Optional features — false unless the client bought them
FEATURE_EMAIL_SMS=false
FEATURE_DIRECT_MAIL=false
FEATURE_BLOG=false
FEATURE_PAYMENTS=false
FEATURE_SOCIAL=false
FEATURE_SOCIAL_AI=false

Always-on branding / infra (not feature flags): SITE_NAME, SITE_TAGLINE, PUBLIC_SITE_URL, CONTACT_*, DJANGO_SECRET_KEY, DATABASE_URL, RECAPTCHA_*, TIANJI_*, etc.

Provider secrets are only required when the matching flag is true. scripts/validate-env.sh must fail boot if a flag is on and its secrets are missing:

Flag Required secrets
FEATURE_EMAIL_SMS EMAIL_HOST_USER, EMAIL_HOST_PASSWORD, SMTP2GO_SMS_API_KEY, SMTP2GO_WEBHOOK_SECRET
FEATURE_DIRECT_MAIL PCM_API_KEY, PCM_API_SECRET, PCM_WEBHOOK_SECRETS, PCM_RETURN_ADDRESS
FEATURE_PAYMENTS STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY, STRIPE_WEBHOOK_SECRET
FEATURE_SOCIAL SOCIAL_TOKEN_ENCRYPTION_KEY (Meta/LinkedIn app creds can stay portal-UI)
FEATURE_SOCIAL_AI OLLAMA_BASE_URL (default http://10.0.0.128:11434), OLLAMA_MODEL
FEATURE_BLOG none extra

Ollama is the LAN instance at 10.0.0.128:11434. App hosts must reach it; do not call a public Ollama.


How flags turn into apps (hard isolation)

Two layers, both required:

  1. Secret flag → settings decides membership of INSTALLED_APPS.
  2. App not installed → Django never loads its models, URLs, admin, middleware, or AppConfig.ready().

A boolean in a template is not enough. If FEATURE_SOCIAL=false, social is absent from INSTALLED_APPS.

Settings

# settings/base.py (sketch)
FEATURE_EMAIL_SMS = env_bool("FEATURE_EMAIL_SMS", False)
FEATURE_DIRECT_MAIL = env_bool("FEATURE_DIRECT_MAIL", False)
FEATURE_BLOG = env_bool("FEATURE_BLOG", False)
FEATURE_PAYMENTS = env_bool("FEATURE_PAYMENTS", False)
FEATURE_SOCIAL = env_bool("FEATURE_SOCIAL", False)
FEATURE_SOCIAL_AI = env_bool("FEATURE_SOCIAL_AI", False)

if FEATURE_PAYMENTS and not FEATURE_EMAIL_SMS:
    raise ImproperlyConfigured("FEATURE_PAYMENTS requires FEATURE_EMAIL_SMS")
if FEATURE_SOCIAL_AI and not FEATURE_SOCIAL:
    raise ImproperlyConfigured("FEATURE_SOCIAL_AI requires FEATURE_SOCIAL")

CORE_APPS = [
    "core.apps.CoreConfig",
    "public.apps.PublicConfig",
    "accounts.apps.AccountsConfig",
    "dashboard.apps.DashboardConfig",
    "leads.apps.LeadsConfig",
    "contacts.apps.ContactsConfig",
    "analytics.apps.AnalyticsConfig",
    # django.contrib.* + third-party always needed
]

OPTIONAL_APPS = [
    (FEATURE_EMAIL_SMS, "email_sms.apps.EmailSmsConfig"),
    (FEATURE_DIRECT_MAIL, "directmail.apps.DirectmailConfig"),
    (FEATURE_BLOG, "blog.apps.BlogConfig"),
    (FEATURE_PAYMENTS, "payments.apps.PaymentsConfig"),
    (FEATURE_SOCIAL, "social.apps.SocialConfig"),
    (FEATURE_SOCIAL_AI, "social_ai.apps.SocialAiConfig"),
]

INSTALLED_APPS = CORE_APPS + [app for enabled, app in OPTIONAL_APPS if enabled]

Also gate optional middleware the same way (do not add social / email_sms middleware when the app is off).

URLs — only include installed apps

from django.apps import apps

urlpatterns = [
    path("healthz/", healthz),
    path("admin/", admin.site.urls),
    path("accounts/", include("accounts.urls")),
    path("portal/", include("dashboard.urls")),
    path("portal/leads/", include("leads.urls")),
    path("portal/analytics/", include("analytics.urls")),
    path("", include("public.urls")),
]

if apps.is_installed("email_sms"):
    urlpatterns += [path("portal/messaging/", include("email_sms.urls"))]
if apps.is_installed("directmail"):
    urlpatterns += [path("portal/direct-mail/", include("directmail.urls"))]
if apps.is_installed("blog"):
    urlpatterns += [
        path("blog/", include("blog.public_urls")),
        path("portal/blog/", include("blog.portal_urls")),
    ]
if apps.is_installed("payments"):
    urlpatterns += [path("portal/payments/", include("payments.urls"))]
if apps.is_installed("social"):
    urlpatterns += [path("portal/social/", include("social.urls"))]
if apps.is_installed("social_ai"):
    urlpatterns += [path("portal/social/api/generate/", include("social_ai.urls"))]

Use django.apps.apps.is_installed("app_label"), not a second copy of the env flag, so URLs cannot drift from INSTALLED_APPS.

Nav, dashboard widgets, context processors — registry, not hard imports

Always-on apps must not from email_sms.models import … or {% url 'messaging:campaign_list' %} at import/render time.

Pattern: optional apps register in AppConfig.ready():

  • portal nav items
  • dashboard home widgets
  • public-header links (blog)

If the app is not installed, ready() never runs → nothing registered → no link, no widget, no {% url %} to a missing namespace.

dashboard.views.home in monica_site currently imports Campaign and SocialPost always. That is the anti-pattern this template must not copy.

Cross-app imports

Allowed:

  • optional → always-on (email_smscontacts, social_aisocial)
  • optional → optional only for declared deps (paymentsemail_sms, social_aisocial)

Forbidden:

  • always-on → optional at module import time
  • social importing social_ai (AI is an add-on; social works without it)
  • email_sms importing directmail or payments

Use apps.is_installed(...) + local import inside a function if an always-on view needs an optional hook. Prefer the registry.

Migrations

manage.py migrate only migrates installed apps. A client without FEATURE_SOCIAL never creates social tables. Turning a feature on later: set flag, redeploy, migrate. Turning off: flag false, app gone; leave unused tables in Postgres (do not auto-drop).


New client bootstrap (faster next time)

  1. Create repo from this Gitea template (web_django_template<client>_site). Same naming as monica_site / company_site.
  2. Rename project package if the template uses a placeholder (client_sitemonica_site). Provide a script: scripts/bootstrap-client.sh <slug> <Site Name> <domain> that rewrites package name, SITE_NAME defaults, compose project name, server-infra catalog key comments.
  3. Brand public templates / static/brand/ (logo, colors, copy). Keep layout; swap assets.
  4. Secrets on the control node: copy .env.prod.example~/Documents/secrets/<client>_site/. Set DJANGO_SECRET_KEY, DATABASE_URL, domain, recaptcha, Tianji website id. Flip only the FEATURE_* flags they bought. Fill provider secrets for those flags.
  5. Infra: Postgres DB on 10.0.0.230; server-infra app_catalog entry + host ports; NPM vhost; Gitea CI deploy (same pattern as monica_site).
  6. Deploy. Smoke: public home/contact, portal login/logout, leads, UTM. Confirm disabled feature URLs 404 and do not appear in portal nav.
  7. Create portal user (createsuperuser / staff).

Catalog defaults: public + portal always on → $600 build / $40/mo before add-ons. Add-ons are flags, not extra repos.


Per-app responsibilities (extract / build)

Always-on

  • core: TimeStampedModel, healthz, nav/widget registry, dispatch_due if still needed.
  • public: landing, about, contact (+ extra service pages per client), reCAPTCHA, SEO, under-construction gate.
  • accounts: login, logout, password change, realtor profile.
  • dashboard: portal shell, home that only renders registered widgets.
  • leads: lead inbox, statuses, notes; contact-form creates Lead + Contact.
  • analytics: UTM middleware, attribution, portal reports. Tianji stays env-based (pageviews), not a SKU.
  • contacts: person record + consent/suppression tables used by optional channels. Mailing-list UI lives in email_sms, not here.

email_sms (FEATURE_EMAIL_SMS)

SMTP2GO email + SMS. Templates, campaigns, per-recipient messages, webhooks, unsubscribe / STOP, engagement reports, mailing-list UI. Django Tasks senders. Do not include postcard.

directmail (FEATURE_DIRECT_MAIL)

PCM Integrations (DirectMail API v3): iframe designer, orders, webhooks, postcard campaigns. Postage billed outside the app. Own models; may read contacts for recipients + postcard consent.

blog (FEATURE_BLOG)

Public /blog/ + portal post CRUD. Registers a public-nav item only when installed.

payments (FEATURE_PAYMENTS)

Stripe invoices + pay links. Sends invoice email through email_sms (hence the dependency). Stripe usage billed separately. Boot fails if this flag is on and FEATURE_EMAIL_SMS is off.

social (FEATURE_SOCIAL)

Connected accounts, composer, scheduling, Meta/LinkedIn connectors, Django Tasks publish. No Ollama / generate endpoint.

social_ai (FEATURE_SOCIAL_AI)

Ollama drafts against OLLAMA_BASE_URL (default http://10.0.0.128:11434). Registers generate API + composer “Draft with AI” only when installed. Imports social; social never imports social_ai.


Tests that lock isolation

For each optional app, a settings override with that flag false:

  • app label not in django.apps.apps.get_app_configs()
  • its URL prefix returns 404
  • portal HTML does not contain its nav href
  • always-on modules import without that app installed

For each declared dependency: enabling the child without the parent raises ImproperlyConfigured at settings load.


Acceptance

  • All six optional catalog features are separate Django apps (not mixed channels inside one app).
  • FEATURE_* flags in secret env files are the only way to put those apps in INSTALLED_APPS.
  • Flag off → no URLs, no nav, no dashboard widget, no admin, no migrations applied for that app.
  • FEATURE_PAYMENTS requires FEATURE_EMAIL_SMS; FEATURE_SOCIAL_AI requires FEATURE_SOCIAL; boot error otherwise.
  • Always-on code never imports optional apps at module level (monica_site dashboard home is the example not to copy).
  • scripts/bootstrap-client.sh (or equivalent) + documented secrets path so a new client is: template → rename → flags → infra → deploy.
  • .env.example / .env.prod.example document every flag and the provider secrets each flag requires.
  • Ollama default is the LAN host http://10.0.0.128:11434.

Out of scope for this ticket

Implementing Stripe/blog/PCM from scratch in a live client. This ticket is the control plane and app boundaries. Fill app bodies from monica_site under #1, splitting messaging and social as specified here.

## Why This repo is the Gitea template for client websites ([Web Design & Hosting catalog](https://aimloperations.com/web_design)). `monica_site` is the first instantiation — it works, but **every feature is always installed**. Dashboard, URLs, and models import `messaging` and `social` unconditionally. Next client with a smaller package should not get Email/SMS, PCM, blog, Stripe, social, or Ollama drafts unless those features are purchased and turned on. Goal: clone this template, set branding + **secret feature flags**, deploy. Disabled features are not in `INSTALLED_APPS`, so they have no URLs, models, admin, nav, or templates. Guessing `/portal/social/` 404s. No flag-only hide in templates while the app still loads. Related: #1 (populate the template from `monica_site`). Follow this ticket when extracting. --- ## Catalog → Django apps Always included (no flag; always in `INSTALLED_APPS`): | Catalog item | Django app(s) | Notes | |---|---|---| | Public site (landing, about, contact + service pages) | `public` | reCAPTCHA contact form | | Client portal + UTM (login, dashboard, leads, UTM) | `accounts`, `dashboard`, `leads`, `analytics` | login / logout / session | | Shared platform | `core`, `contacts` | `contacts` = people store for leads + optional channels | Optional add-ons — **one Django app per catalog feature**: | Catalog item | App | Secret flag | Depends on | |---|---|---|---| | Email & SMS (SMTP2GO campaigns, mailing list, engagement) | `email_sms` | `FEATURE_EMAIL_SMS` | — | | Direct mail (PCM postcard designer + print/send) | `directmail` | `FEATURE_DIRECT_MAIL` | — | | Blog (public blog + portal post mgmt) | `blog` | `FEATURE_BLOG` | — | | Payments (Stripe invoices + pay links) | `payments` | `FEATURE_PAYMENTS` | `FEATURE_EMAIL_SMS` | | Social consolidation (accounts, composer, scheduling) | `social` | `FEATURE_SOCIAL` | — | | AI social generator (Ollama drafts) | `social_ai` | `FEATURE_SOCIAL_AI` | `FEATURE_SOCIAL` | **Do not** keep postcard inside `email_sms` / `messaging`, and **do not** keep Ollama generate inside `social`. Those are separate SKUs. `monica_site` today: `messaging` = email + SMS + PCM; `social` includes `api/generate/` + `social.ollama`. Split when extracting. --- ## Secret flags (source of truth) Flags live in the **deploy secret env files**, same place as `DJANGO_SECRET_KEY` — never committed: ``` ~/Documents/secrets/<client>_site/<client>_site_prod.env ~/Documents/secrets/<client>_site/<client>_site_beta.env ``` Template ships `.env.example` / `.env.prod.example` with the keys documented, values empty/false. ```bash # Optional features — false unless the client bought them FEATURE_EMAIL_SMS=false FEATURE_DIRECT_MAIL=false FEATURE_BLOG=false FEATURE_PAYMENTS=false FEATURE_SOCIAL=false FEATURE_SOCIAL_AI=false ``` Always-on branding / infra (not feature flags): `SITE_NAME`, `SITE_TAGLINE`, `PUBLIC_SITE_URL`, `CONTACT_*`, `DJANGO_SECRET_KEY`, `DATABASE_URL`, `RECAPTCHA_*`, `TIANJI_*`, etc. Provider secrets are **only required when the matching flag is true**. `scripts/validate-env.sh` must fail boot if a flag is on and its secrets are missing: | Flag | Required secrets | |---|---| | `FEATURE_EMAIL_SMS` | `EMAIL_HOST_USER`, `EMAIL_HOST_PASSWORD`, `SMTP2GO_SMS_API_KEY`, `SMTP2GO_WEBHOOK_SECRET` | | `FEATURE_DIRECT_MAIL` | `PCM_API_KEY`, `PCM_API_SECRET`, `PCM_WEBHOOK_SECRETS`, `PCM_RETURN_ADDRESS` | | `FEATURE_PAYMENTS` | `STRIPE_SECRET_KEY`, `STRIPE_PUBLISHABLE_KEY`, `STRIPE_WEBHOOK_SECRET` | | `FEATURE_SOCIAL` | `SOCIAL_TOKEN_ENCRYPTION_KEY` (Meta/LinkedIn app creds can stay portal-UI) | | `FEATURE_SOCIAL_AI` | `OLLAMA_BASE_URL` (default `http://10.0.0.128:11434`), `OLLAMA_MODEL` | | `FEATURE_BLOG` | none extra | Ollama is the LAN instance at **10.0.0.128:11434**. App hosts must reach it; do not call a public Ollama. --- ## How flags turn into apps (hard isolation) Two layers, both required: 1. **Secret flag** → settings decides membership of `INSTALLED_APPS`. 2. **App not installed** → Django never loads its models, URLs, admin, middleware, or `AppConfig.ready()`. A boolean in a template is **not** enough. If `FEATURE_SOCIAL=false`, `social` is absent from `INSTALLED_APPS`. ### Settings ```python # settings/base.py (sketch) FEATURE_EMAIL_SMS = env_bool("FEATURE_EMAIL_SMS", False) FEATURE_DIRECT_MAIL = env_bool("FEATURE_DIRECT_MAIL", False) FEATURE_BLOG = env_bool("FEATURE_BLOG", False) FEATURE_PAYMENTS = env_bool("FEATURE_PAYMENTS", False) FEATURE_SOCIAL = env_bool("FEATURE_SOCIAL", False) FEATURE_SOCIAL_AI = env_bool("FEATURE_SOCIAL_AI", False) if FEATURE_PAYMENTS and not FEATURE_EMAIL_SMS: raise ImproperlyConfigured("FEATURE_PAYMENTS requires FEATURE_EMAIL_SMS") if FEATURE_SOCIAL_AI and not FEATURE_SOCIAL: raise ImproperlyConfigured("FEATURE_SOCIAL_AI requires FEATURE_SOCIAL") CORE_APPS = [ "core.apps.CoreConfig", "public.apps.PublicConfig", "accounts.apps.AccountsConfig", "dashboard.apps.DashboardConfig", "leads.apps.LeadsConfig", "contacts.apps.ContactsConfig", "analytics.apps.AnalyticsConfig", # django.contrib.* + third-party always needed ] OPTIONAL_APPS = [ (FEATURE_EMAIL_SMS, "email_sms.apps.EmailSmsConfig"), (FEATURE_DIRECT_MAIL, "directmail.apps.DirectmailConfig"), (FEATURE_BLOG, "blog.apps.BlogConfig"), (FEATURE_PAYMENTS, "payments.apps.PaymentsConfig"), (FEATURE_SOCIAL, "social.apps.SocialConfig"), (FEATURE_SOCIAL_AI, "social_ai.apps.SocialAiConfig"), ] INSTALLED_APPS = CORE_APPS + [app for enabled, app in OPTIONAL_APPS if enabled] ``` Also gate optional **middleware** the same way (do not add `social` / `email_sms` middleware when the app is off). ### URLs — only include installed apps ```python from django.apps import apps urlpatterns = [ path("healthz/", healthz), path("admin/", admin.site.urls), path("accounts/", include("accounts.urls")), path("portal/", include("dashboard.urls")), path("portal/leads/", include("leads.urls")), path("portal/analytics/", include("analytics.urls")), path("", include("public.urls")), ] if apps.is_installed("email_sms"): urlpatterns += [path("portal/messaging/", include("email_sms.urls"))] if apps.is_installed("directmail"): urlpatterns += [path("portal/direct-mail/", include("directmail.urls"))] if apps.is_installed("blog"): urlpatterns += [ path("blog/", include("blog.public_urls")), path("portal/blog/", include("blog.portal_urls")), ] if apps.is_installed("payments"): urlpatterns += [path("portal/payments/", include("payments.urls"))] if apps.is_installed("social"): urlpatterns += [path("portal/social/", include("social.urls"))] if apps.is_installed("social_ai"): urlpatterns += [path("portal/social/api/generate/", include("social_ai.urls"))] ``` Use `django.apps.apps.is_installed("app_label")`, not a second copy of the env flag, so URLs cannot drift from `INSTALLED_APPS`. ### Nav, dashboard widgets, context processors — registry, not hard imports Always-on apps **must not** `from email_sms.models import …` or `{% url 'messaging:campaign_list' %}` at import/render time. Pattern: optional apps register in `AppConfig.ready()`: * portal nav items * dashboard home widgets * public-header links (blog) If the app is not installed, `ready()` never runs → nothing registered → no link, no widget, no `{% url %}` to a missing namespace. `dashboard.views.home` in `monica_site` currently imports `Campaign` and `SocialPost` always. That is the anti-pattern this template must not copy. ### Cross-app imports Allowed: * optional → always-on (`email_sms` → `contacts`, `social_ai` → `social`) * optional → optional **only** for declared deps (`payments` → `email_sms`, `social_ai` → `social`) Forbidden: * always-on → optional at module import time * `social` importing `social_ai` (AI is an add-on; social works without it) * `email_sms` importing `directmail` or `payments` Use `apps.is_installed(...)` + local import inside a function if an always-on view needs an optional hook. Prefer the registry. ### Migrations `manage.py migrate` only migrates installed apps. A client without `FEATURE_SOCIAL` never creates social tables. Turning a feature **on** later: set flag, redeploy, migrate. Turning **off**: flag false, app gone; leave unused tables in Postgres (do not auto-drop). --- ## New client bootstrap (faster next time) 1. **Create repo from this Gitea template** (`web_django_template` → `<client>_site`). Same naming as `monica_site` / `company_site`. 2. **Rename project package** if the template uses a placeholder (`client_site` → `monica_site`). Provide a script: `scripts/bootstrap-client.sh <slug> <Site Name> <domain>` that rewrites package name, `SITE_NAME` defaults, compose project name, `server-infra` catalog key comments. 3. **Brand** public templates / `static/brand/` (logo, colors, copy). Keep layout; swap assets. 4. **Secrets** on the control node: copy `.env.prod.example` → `~/Documents/secrets/<client>_site/`. Set `DJANGO_SECRET_KEY`, `DATABASE_URL`, domain, recaptcha, Tianji website id. Flip **only** the `FEATURE_*` flags they bought. Fill provider secrets for those flags. 5. **Infra**: Postgres DB on `10.0.0.230`; `server-infra` `app_catalog` entry + host ports; NPM vhost; Gitea CI deploy (same pattern as `monica_site`). 6. **Deploy**. Smoke: public home/contact, portal login/logout, leads, UTM. Confirm **disabled** feature URLs 404 and do not appear in portal nav. 7. **Create portal user** (`createsuperuser` / staff). Catalog defaults: public + portal always on → `$600` build / `$40`/mo before add-ons. Add-ons are flags, not extra repos. --- ## Per-app responsibilities (extract / build) ### Always-on - **`core`**: `TimeStampedModel`, healthz, nav/widget registry, `dispatch_due` if still needed. - **`public`**: landing, about, contact (+ extra service pages per client), reCAPTCHA, SEO, under-construction gate. - **`accounts`**: login, logout, password change, realtor profile. - **`dashboard`**: portal shell, home that only renders registered widgets. - **`leads`**: lead inbox, statuses, notes; contact-form creates Lead + Contact. - **`analytics`**: UTM middleware, attribution, portal reports. Tianji stays env-based (pageviews), not a SKU. - **`contacts`**: person record + consent/suppression tables used by optional channels. Mailing-list **UI** lives in `email_sms`, not here. ### `email_sms` (`FEATURE_EMAIL_SMS`) SMTP2GO email + SMS. Templates, campaigns, per-recipient messages, webhooks, unsubscribe / `STOP`, engagement reports, mailing-list UI. Django Tasks senders. Do **not** include postcard. ### `directmail` (`FEATURE_DIRECT_MAIL`) PCM Integrations (DirectMail API v3): iframe designer, orders, webhooks, postcard campaigns. Postage billed outside the app. Own models; may read `contacts` for recipients + postcard consent. ### `blog` (`FEATURE_BLOG`) Public `/blog/` + portal post CRUD. Registers a public-nav item only when installed. ### `payments` (`FEATURE_PAYMENTS`) Stripe invoices + pay links. Sends invoice email through `email_sms` (hence the dependency). Stripe usage billed separately. Boot fails if this flag is on and `FEATURE_EMAIL_SMS` is off. ### `social` (`FEATURE_SOCIAL`) Connected accounts, composer, scheduling, Meta/LinkedIn connectors, Django Tasks publish. **No** Ollama / generate endpoint. ### `social_ai` (`FEATURE_SOCIAL_AI`) Ollama drafts against `OLLAMA_BASE_URL` (default `http://10.0.0.128:11434`). Registers generate API + composer “Draft with AI” only when installed. Imports `social`; `social` never imports `social_ai`. --- ## Tests that lock isolation For each optional app, a settings override with that flag **false**: - app label not in `django.apps.apps.get_app_configs()` - its URL prefix returns 404 - portal HTML does not contain its nav href - always-on modules import without that app installed For each declared dependency: enabling the child without the parent raises `ImproperlyConfigured` at settings load. --- ## Acceptance - [ ] All six optional catalog features are separate Django apps (not mixed channels inside one app). - [ ] `FEATURE_*` flags in secret env files are the only way to put those apps in `INSTALLED_APPS`. - [ ] Flag off → no URLs, no nav, no dashboard widget, no admin, no migrations applied for that app. - [ ] `FEATURE_PAYMENTS` requires `FEATURE_EMAIL_SMS`; `FEATURE_SOCIAL_AI` requires `FEATURE_SOCIAL`; boot error otherwise. - [ ] Always-on code never imports optional apps at module level (`monica_site` dashboard home is the example not to copy). - [ ] `scripts/bootstrap-client.sh` (or equivalent) + documented secrets path so a new client is: template → rename → flags → infra → deploy. - [ ] `.env.example` / `.env.prod.example` document every flag and the provider secrets each flag requires. - [ ] Ollama default is the LAN host `http://10.0.0.128:11434`. ## Out of scope for this ticket Implementing Stripe/blog/PCM from scratch in a live client. This ticket is the **control plane** and app boundaries. Fill app bodies from `monica_site` under #1, splitting `messaging` and `social` as specified here.
Author
Owner

Implemented on master in 787f0e4.

  • FEATURE_* in secret env → membership of INSTALLED_APPS
  • Optional apps: email_sms, directmail, blog, payments, social, social_ai
  • URLs via apps.is_installed(...); nav/widgets via core.registry from AppConfig.ready()
  • Boot fails if payments without email/SMS, or social AI without social
  • .env.example / .env.prod.example + scripts/validate-env.sh

Populate work is #1.

Implemented on master in [`787f0e4`](https://git.aimloperations.com/westfarn/web_django_template/commit/787f0e48fbde1ec39a77705dfd71b708f6db3f91). - `FEATURE_*` in secret env → membership of `INSTALLED_APPS` - Optional apps: `email_sms`, `directmail`, `blog`, `payments`, `social`, `social_ai` - URLs via `apps.is_installed(...)`; nav/widgets via `core.registry` from `AppConfig.ready()` - Boot fails if payments without email/SMS, or social AI without social - `.env.example` / `.env.prod.example` + `scripts/validate-env.sh` Populate work is #1.
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: westfarn/web_django_template#2