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)
Template ships .env.example / .env.prod.example with the keys documented, values empty/false.
# Optional features — false unless the client bought themFEATURE_EMAIL_SMS=falseFEATURE_DIRECT_MAIL=falseFEATURE_BLOG=falseFEATURE_PAYMENTS=falseFEATURE_SOCIAL=falseFEATURE_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:
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)
Create repo from this Gitea template (web_django_template → <client>_site). Same naming as monica_site / company_site.
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.
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.
Infra: Postgres DB on 10.0.0.230; server-infraapp_catalog entry + host ports; NPM vhost; Gitea CI deploy (same pattern as monica_site).
Deploy. Smoke: public home/contact, portal login/logout, leads, UTM. Confirm disabled feature URLs 404 and do not appear in portal nav.
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.
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.
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.
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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Why
This repo is the Gitea template for client websites (Web Design & Hosting catalog).
monica_siteis the first instantiation — it works, but every feature is always installed. Dashboard, URLs, and models importmessagingandsocialunconditionally. 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):publicaccounts,dashboard,leads,analyticscore,contactscontacts= people store for leads + optional channelsOptional add-ons — one Django app per catalog feature:
email_smsFEATURE_EMAIL_SMSdirectmailFEATURE_DIRECT_MAILblogFEATURE_BLOGpaymentsFEATURE_PAYMENTSFEATURE_EMAIL_SMSsocialFEATURE_SOCIALsocial_aiFEATURE_SOCIAL_AIFEATURE_SOCIALDo not keep postcard inside
email_sms/messaging, and do not keep Ollama generate insidesocial. Those are separate SKUs.monica_sitetoday:messaging= email + SMS + PCM;socialincludesapi/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:Template ships
.env.example/.env.prod.examplewith the keys documented, values empty/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.shmust fail boot if a flag is on and its secrets are missing:FEATURE_EMAIL_SMSEMAIL_HOST_USER,EMAIL_HOST_PASSWORD,SMTP2GO_SMS_API_KEY,SMTP2GO_WEBHOOK_SECRETFEATURE_DIRECT_MAILPCM_API_KEY,PCM_API_SECRET,PCM_WEBHOOK_SECRETS,PCM_RETURN_ADDRESSFEATURE_PAYMENTSSTRIPE_SECRET_KEY,STRIPE_PUBLISHABLE_KEY,STRIPE_WEBHOOK_SECRETFEATURE_SOCIALSOCIAL_TOKEN_ENCRYPTION_KEY(Meta/LinkedIn app creds can stay portal-UI)FEATURE_SOCIAL_AIOLLAMA_BASE_URL(defaulthttp://10.0.0.128:11434),OLLAMA_MODELFEATURE_BLOGOllama 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:
INSTALLED_APPS.AppConfig.ready().A boolean in a template is not enough. If
FEATURE_SOCIAL=false,socialis absent fromINSTALLED_APPS.Settings
Also gate optional middleware the same way (do not add
social/email_smsmiddleware when the app is off).URLs — only include installed apps
Use
django.apps.apps.is_installed("app_label"), not a second copy of the env flag, so URLs cannot drift fromINSTALLED_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():If the app is not installed,
ready()never runs → nothing registered → no link, no widget, no{% url %}to a missing namespace.dashboard.views.homeinmonica_sitecurrently importsCampaignandSocialPostalways. That is the anti-pattern this template must not copy.Cross-app imports
Allowed:
email_sms→contacts,social_ai→social)payments→email_sms,social_ai→social)Forbidden:
socialimportingsocial_ai(AI is an add-on; social works without it)email_smsimportingdirectmailorpaymentsUse
apps.is_installed(...)+ local import inside a function if an always-on view needs an optional hook. Prefer the registry.Migrations
manage.py migrateonly migrates installed apps. A client withoutFEATURE_SOCIALnever 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)
web_django_template→<client>_site). Same naming asmonica_site/company_site.client_site→monica_site). Provide a script:scripts/bootstrap-client.sh <slug> <Site Name> <domain>that rewrites package name,SITE_NAMEdefaults, compose project name,server-infracatalog key comments.static/brand/(logo, colors, copy). Keep layout; swap assets..env.prod.example→~/Documents/secrets/<client>_site/. SetDJANGO_SECRET_KEY,DATABASE_URL, domain, recaptcha, Tianji website id. Flip only theFEATURE_*flags they bought. Fill provider secrets for those flags.10.0.0.230;server-infraapp_catalogentry + host ports; NPM vhost; Gitea CI deploy (same pattern asmonica_site).createsuperuser/ staff).Catalog defaults: public + portal always on →
$600build /$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_dueif 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 inemail_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
contactsfor 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 andFEATURE_EMAIL_SMSis 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(defaulthttp://10.0.0.128:11434). Registers generate API + composer “Draft with AI” only when installed. Importssocial;socialnever importssocial_ai.Tests that lock isolation
For each optional app, a settings override with that flag false:
django.apps.apps.get_app_configs()For each declared dependency: enabling the child without the parent raises
ImproperlyConfiguredat settings load.Acceptance
FEATURE_*flags in secret env files are the only way to put those apps inINSTALLED_APPS.FEATURE_PAYMENTSrequiresFEATURE_EMAIL_SMS;FEATURE_SOCIAL_AIrequiresFEATURE_SOCIAL; boot error otherwise.monica_sitedashboard 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.exampledocument every flag and the provider secrets each flag requires.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_siteunder #1, splittingmessagingandsocialas specified here.Implemented on master in
787f0e4.FEATURE_*in secret env → membership ofINSTALLED_APPSemail_sms,directmail,blog,payments,social,social_aiapps.is_installed(...); nav/widgets viacore.registryfromAppConfig.ready().env.example/.env.prod.example+scripts/validate-env.shPopulate work is #1.