The sign-in page (frontend/js/accounts/views/LoginView.vue) now automatically attempts passkey/WebAuthn sign-in on mount when the browser supports it, opening the credential selector without requiring a click. The existing Sign in with a passkey button is preserved as an explicit fallback/manual trigger. A new attemptPasskeyLogin(isAuto) helper backs both paths: auto attempts stay silent on common gesture-related rejections (NotAllowedError / AbortError), while explicit button clicks retain the full loading/error UX. A passkeyInFlight guard prevents the auto and manual triggers from overlapping.
Documentation system migrated from MkDocs + Material for MkDocs to Zensical (the official successor by the same team).
Configuration moved to native zensical.toml at the project root (replaced config/mkdocs.yml).
Removed mkdocs-include-markdown-plugin. Content includes now use pymdownx.snippets (e.g. docs/index.md pulls sections from README.md; docs/changelog.md includes the full CHANGELOG.md).
Restored docs/index.md to include the main README content via snippets, plus a new "Working on the Documentation" section.
New Just commands: just docs (serve on port 4000), just docs-build, just docs-lint (added to config/base.just in alphabetical order).
Local documentation server now runs on port 4000 by default (dev_addr in zensical.toml) to avoid conflict with Django.
Removed the docs service and full/docs profiles from compose.yml.
Updated .readthedocs.yaml to use zensical build.
Navigation improvements: added navigation.indexes, set title: Overview on the index page via frontmatter, and expanded features in zensical.toml while remaining on the classic theme variant.
Cleaned up old MkDocs references across the project (README, CLAUDE.md, docs, etc.).
2026-05-23
Added
apps/billing/example_plans.py — bundled Free / Pro / Business three-tier demo, opt-in via the new BILLING_USE_EXAMPLE_PLANS env var (registered in .env.toml, default false). When the flag is on, config/settings/_base.py imports BILLING_PLANS and BILLING_FEATURES from the example module instead of leaving them empty. Lets the maintainer (and anyone evaluating the template) dogfood the billing UX without editing settings and dragging a 70-line diff through unrelated work. The conditional import only fires when the flag is set, so the default startup path never touches apps.billing. See the new "Dogfooding locally" section in docs/billing.md.
python manage.py seed_example_billing — idempotent Stripe seeder that creates Pro + Business products and prices in test mode, then prints the four STRIPE_PRICE_* env-var lines to paste into .env. Pairs with BILLING_USE_EXAMPLE_PLANS=true for a one-command dogfood setup. Idempotency comes from Stripe's lookup_key on prices and a djbs_seed_key metadata marker on products — rerunning the command never creates duplicates. Refuses to run against sk_live_… keys unless --force-live is passed. The /pricing/ empty-state page now points staff users at this command with a three-step recipe instead of the one-liner "add entries to BILLING_PLANS".
2026-05-22
Added
Django superuser hook via just init + epicenv create-superuser. New DJANGO_SUPERUSER_USERNAME / _EMAIL / _PASSWORD variables in .env.toml declare the credentials (left blank by default); just init brings services up, runs the new idempotent create_superuser recipe in the top-level justfile, then hands off to just start. The recipe skips with a friendly message when DJANGO_SUPERUSER_USERNAME is blank, brings the web container up if it isn't already, and is safe to run standalone any time (e.g., after rotating the admin password). Teams using a secrets manager edit the create_superuser recipe in place to pipe credentials from 1Password (or similar) into docker compose exec -T web epicenv create-superuser. scripts/start_new_project now instructs developers to run just init the first time and just start for every subsequent boot.
Changed
Upgraded epicenv[django] to v1.6.2. Bumps the uvx epicenv@… pins in scripts/start_new_project and the just create_env recipe to 1.6.2, and refreshes the [tool.uv.exclude-newer-package] settle date so uv sync (both locally and in Dockerfile.web) can resolve the new release. v1.6.2 also patches a non-blocking-stdin bug in epicenv create-superuser that made it miss JSON piped from any slow upstream (uvx epicenv secrets get …, op item get, etc.) — the secrets-manager example in the create_superuser recipe now works as a plain pipeline without needing a bash-shebang workaround.
2026-05-15
Changed
Upgraded epicenv[django] to v1.4 and moved the env schema from [tool.epicenv.variables] in pyproject.toml to a dedicated .env.toml file at the project root. Every variable uses the multi-line [variables.NAME] form for a uniform layout.
Pinned the exact epicenv version used by scripts/start_new_project and the just create_env recipe (uvx epicenv@1.4.0 create) so new project bootstraps remain reproducible.
2026-05-09 (later)
Added
apps/billing/ — opt-in Stripe subscriptions tied to Organizations. Models: BillingCustomer (one Stripe customer per org, survives cancellation), Subscription (local mirror of the active Stripe sub with status / billing_cycle / quantity / period and trial dates), WebhookEvent (idempotency dedupe for Stripe retries). Settings-declared plan + feature registries follow the NOTIFICATIONS_CATEGORIES pattern (BILLING_PLANS and BILLING_FEATURES in _base.py); plans are loaded into apps/billing/plans.pyPlan dataclasses and features into apps/billing/features.pyFeature dataclasses.
Ninja API at /api/billing/ (mounted only when BILLING_ENABLED=true): GET /plans/ + GET /features/ (public, drive the pricing page), GET /subscription/ (owner-only current state), POST /checkout/ (returns Stripe Checkout URL — full-page redirect), POST /portal/ (returns Stripe Customer Portal URL — handles upgrade/downgrade/cancel/payment-methods/invoices). Checkout sets allow_promotion_codes=True so coupon codes work; subscription_data.trial_period_days is set on the first subscription only (Stripe rejects trials on subsequent subs for the same customer).
Stripe webhook at /webhooks/stripe/ (mounted only when BILLING_ENABLED=true, registered in config/urls.py before the SPA catch-all). Verifies signatures with stripe.Webhook.construct_event, dedupes via WebhookEvent, handles checkout.session.completed, customer.subscription.{created,updated,deleted}, invoice.payment_{succeeded,failed} — sends in-app notifications + emails on payment_failed and subscription_deleted.
org_has_feature(org, key) / org_feature_value(org, key) helpers in apps/billing/access.py plus a requires_feature decorator that 402-Payment-Required's missing features. When BILLING_ENABLED=False, gates fall through to feature defaults so the starter template runs out of the box without Stripe credentials.
Per-seat pricing — OrganizationMemberpost_save/post_delete receivers schedule transaction.on_commit(sync_seat_quantity) so the Stripe quantity update fires after the membership change commits. No-ops for non-seat-based plans, free plans, and disabled billing.
Trial reminders — check_trials_ending celery beat task (daily 04:00 UTC) sends billing_trial_ending notifications + emails 3 days before trial_end, idempotent via Subscription.trial_ending_notified_at.
Drift recovery — reconcile_subscriptions celery beat task (weekly Mon 05:00 UTC) iterates all BillingCustomer rows and re-syncs from Stripe, defending against missed webhooks.
app_context.billing block in /api/app-context/ — exposes enabled, plan, status, trial_end, cancel_at_period_end, and a resolved features map. SPA store (frontend/js/stores/app.js) surfaces appStore.hasFeature(key) / appStore.featureValue(key, fallback).
/pricing/ SPA page (PricingView.vue) — public (uses MarketingLayout.vue), monthly/annual toggle, plan cards with smart CTA logic (Sign up / Create org / Subscribe / Switch / Current). Hidden when BILLING_ENABLED=false via a router guard.
Org Settings → Billing tab (BillingView.vue, route /organizations/:slug/settings/billing/) — current plan + status badge, trial countdown, "open Stripe portal" button, seat info, change-plan link to /pricing/. Handles the post-checkout webhook race by polling GET /api/billing/subscription/ until the sub becomes active. Tab is conditionally appended in OrgSettingsLayout.vue only when billing is enabled.
<FeatureGate feature="X"> Vue component + useFeature(key) composable for declarative gating; useBilling() composable owns the subscription/plans state and the subscribe / portal / poll-until-active actions.
TrialEndingBanner.vue (dismissible, keyed on trial_end) and PastDueBanner.vue (non-dismissible, links to portal) mounted at the top of AppLayout.vue.
frontend/js/views/settings/TeamsView.vue is now wrapped in <FeatureGate feature="teams"> with an upgrade-prompt fallback so non-paying orgs see a CTA instead of the teams UI.
Email templates under apps/billing/templates/billing/emails/: payment_failed, subscription_canceled, trial_ending (multipart text + HTML, matching the existing template pattern).
BILLING_ENABLED, STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY, STRIPE_WEBHOOK_SECRET registered in the epicenv schema. apps/billing/apps.py:ready() hard-fails at startup with ImproperlyConfigured when billing is enabled but keys are missing or any non-free plan lacks a Stripe price ID.
stripe~=14.0 added to pyproject.toml dependencies.
Changed
apps.base.utils.email.send_email now accepts sending_user=None for system-generated emails (e.g. billing webhook handlers); when None, the Reply-To header is omitted.
apps/base/api.pyapp_context includes a billing block with feature-flag map even when BILLING_ENABLED=False (all features fall through to their declared defaults).
App.vue skips wrapping in AppLayout when the route declares meta.publicChrome=true so the public /pricing/ page renders its own MarketingLayout chrome instead of the in-app nav.
2026-05-09
Added
apps/notifications/ — Notification (recipient + org + GenericForeignKey target) and NotificationPreference models, ninja API at /api/notifications/ (list, unread-count, bulk, per-row patch/delete, prefs), a notify() service helper, category-and-channel preferences (settings.NOTIFICATIONS_CATEGORIES + NotificationChannel), per-row + retention-window purge via the purge_expired_notifications celery beat task and the purge_notifications management command, and a post_delete cleanup signal driven by settings.NOTIFICATIONS_TARGET_MODELS so target deletes don't leave orphans. notify() validates that recipients are members of the supplied organization.
AppNotificationBell.vue + useNotifications.js composable — bell dropdown with Unread/All tabs, select-mode + bulk actions, click-outside, 20-second polling that pauses on visibilitychange, toast on new arrivals, and an AppModal-driven detail view for notifications with no navigable URL. Keyboard accessible: list rows are focusable + Enter/Space trigger actions, Escape closes the dropdown, and the unread badge announces via aria-live.
/notifications/ SPA page — full paginated archive with the same Unread/All tabs, select-mode, and bulk actions as the bell. Linked from the bell footer.
Account settings Notifications tab (AccountNotificationsView.vue) for per-category in-app/email channel toggles.
Embedded celery beat in the worker (celery -A config worker -B) and a CELERY_BEAT_SCHEDULE entry that runs the daily purge at 03:00 UTC via crontab — the comment in _base.py notes that production should run beat as a dedicated process.
NOTIFICATIONS_RETENTION_DAYS env variable (default 90) registered in the epicenv schema.
Changed
Renamed the superuser Send Test Email view (/send-test-emails/) to Test Notifications (/test-notifications/). The form gains a "Send in-app notification" checkbox so the page can exercise both delivery channels; the API endpoint moved from /api/send-test-email/ to /api/test-notifications/ and validates that the recipient is a member of the sender's current org before creating an in-app row.
apps.base.utils.email.send_email now accepts an optional category= argument; when set with User-instance recipients, recipients with the email channel disabled for that category are filtered out via apps.notifications.categories.filter_recipients.
compose.yml worker command: celery -A config worker → celery -A config worker -B so the embedded beat runs the purge schedule for local dev.
.gitignore now excludes celerybeat-schedule* produced by the embedded beat.
2026-05-02
Added
Vue 3 SPA frontend (replaces the Bootstrap 5 + plain JS stack). Mounted as the catch-all front door; Django serves the SPA shell HTML for every non-API path.
apps/organizations/ and apps/teams/ apps as generic multi-tenant SaaS scaffolding (Organization, OrganizationMember with is_owner / is_primary, OrganizationInvite + accept-invite flow, OrganizationMiddleware, server-rendered accept-invite page; SPA org settings tabs for General/Members/Teams; org switcher in the user menu).
Per-user timezone field on User with browser-detection modal and middleware that activates request.user.timezone per request.
django-hijack for staff impersonation (staff-only permission check, SPA impersonate-search view).
django-ninja API at /api/ (app-context, version, send-test-email, users + avatar, organizations, teams).
MinIO service for local media storage; apps/base/storage.py:S3MediaStorage handles the Docker-internal vs browser endpoint URL split.
Playwright e2e tests for the auth flow (pytest-playwright, config/settings/e2e.py).
Tailwind CSS v4 (replaces Bootstrap 5 + Sass) with theme-applied-before-CSS in the SPA shell.
Toast notifications, theme toggle (light/dark/auto), version watcher with deploy-update banner, send-test-emails view (superuser only).
WhiteNoise serving with immutable cache headers for hashed Vite assets in production.
Self-hosted TOTP enrollment QR code (login-required qr_svg view backed by the qrcode package), replacing the previous third-party QR image service.
django-widget-tweaks for the accept-invite template.
Changed
npm -> bun for the JS toolchain (oven/bun:1 Docker image, bun.lock).
uwsgi -> gunicorn (4 workers x 2 threads, gunicorn.conf.py at the repo root).
src/ -> frontend/ (matches the eventual frontend/backend split).
PostgreSQL 17 + Redis 7 + Mailpit + MinIO with healthchecks on every compose service.
SITE_DOMAIN / ALLOWED_HOSTS now default to localhost (not 127.0.0.1) — WebAuthn rejects bare IPs as Relying Party IDs.
vite_asset template tag: dev-mode URLs no longer prepend VITE_OUTPUT_DIR (Vite serves from source paths in dev); vite_asset returns "" for .css requests in dev (CSS is injected via JS HMR); production no longer emits a redundant <link> tag alongside the JS module (the script-module import pulls CSS automatically).
[tool.ty.rules] configured to ignore the django-stubs noise rules (unresolved-attribute, call-non-callable, etc.) since ty's Django integration cannot model dynamic patterns like reverse managers, custom queryset methods, the swappable user model, or ninja's Query/Path/Body sentinels.
public/media/* added to .gitignore (runtime uploads), keeping public/media/.keep.
djLint ignore list extended to H005,H021,H023 so email templates can keep their inline styles, <html> without lang, and entity references.
Custom SignInView, NameChange, SignInForm, NameForm, and the legacy template-based allauth UI (replaced by the SPA + headless allauth + the user PATCH ninja endpoint).
2026-03-05
Added
Added Mailpit for local email capture with a web UI at http://localhost:8025
Changed ACCOUNT_EMAIL_VERIFICATION from "none" to "optional" now that local email delivery works via Mailpit
2026-01-25
Changed
Upgraded epicenv to v1.2 and switched to using the built-in epicenv.initializers.url_safe_password function for generating SECRET_KEY and POSTGRES_PASSWORD
2026-01-24
Added
Added support for remote debugging with VS Code, PyCharm, and LazyVim/Neovim
Added debugpy as a development dependency for Python debugging
Added just create_env command for .env file generation with backup support
Added comprehensive debugging documentation in docs/debugging.md
Added debugging support section to README
Changed
Modernized navbar with offcanvas menu and improved mobile UX
Improved VS Code debugging workflow with simplified two-step process
Refactored ENABLE_DEBUGGER to USE_DEBUGPY for consistency
Refactored .env creation into separate script with backup support
Bumped uv-dependencies group with 7 updates
Bumped development-dependencies group with 2 updates
Bumped Node from 25.3 to 25.4
Fixed
Fixed jumbotron button overflow on mobile devices
Fixed start_new_project script to work on Linux and enhanced CI verification
2025-12-19
Changed
Switched from MyPy to Ty for Python type checking. Ty is a fast, modern type checker from Astral that provides significantly better performance than MyPy.
2025-06-06
Added
Added CLAUDE.md with project overview and development commands for Claude Code
Added Docker Compose healthchecks for PostgreSQL, Redis, and Vite services to ensure reliable service startup
Added Docker build cache mounts for pip, uv, and npm with project-specific IDs to speed up builds
Added .claude/ to .gitignore for local Claude Code settings
Added Docker build cache mounts for apt update and install operations
Changed
Updated Docker Compose depends_on to wait for services to be healthy before starting dependent services
Updated the scripts/start_new_project script with environment variables for docker compose build
2025-05-13
Changed
Changed Pytest to run tests using Postgres instead of SQLite.
2025-05-03
Changed
Switch to using uv sync and the uv.lock with pyproject.toml instead of uv pip with *.in requirement files.
2025-04-13
Changed
Make the default gravatar cartoon-style silhouetted outline of a person
2025-04-12
Changed
Improved the sign in UI/UX
2025-04-05
Changed
Switch to using docker compose exec instead of docker compose run for faster Just commands
Switch to using the base_entrance.html template for Allauth, so templates that aren't overridden like the signup_closed.html template still work
Update the Githut Action that creates a PR for Python upgrades so that it bolds upgrades greater than a patch
Fixed
Fix a JS exception in color picker
Remove the extra double quote in the Github Action that creats a PR for Python upgrades
Fix the browser trying to load the Favicon
Fix the case for "AS" in the docker config file
Fix linting errors
2024-10-04
Fixed
The quickstart script not replacing the project name in the compose.yml file.
2024-10-02
Changed
Updated ESLint to format config files
Suppress SASS warnings until Bootstrap v5.3.4 is released
Added Just commands to dump and restore the database
2024-03-11
Changed
Renamed the Just command build_assets to build_frontend
Moved all common/base Just commands to the config/base.just file
2024-02-26
Changed
Switched to Ruff for linting and formatting Python. This replaces Bandit, Black, and isort.
2024-02-24
Changed
Switched to using UV instead of pip/pip-tools for managing Python requirements
2024-01-06
Changed
Upgraded to Python 3.12
2023-12-31
Added
Added the Just command update_everything to upgrade Python and Node
Changed
Upgraded to Django 5.0
2023-12-26
Changed
Refactor and clean up the vite_asset template tag
Add tests for the vite_asset and vite_hmr_client template tags
2023-12-24
Changed
Upgrade from Vite 4.5 to 5.0
2023-10-07
Added
Add the Django Maintenance Mode package
2023-09-23
Added
Add a gravatar property to the user model
Changed
Make the layout and style better for SaaS projects
2023-09-16
Changed
Updated Redis configuration settings to allow for a REDIS_PREFIX
Added
Added Django-alive for health checks
Added a uwsgi.ini config file
2023-09-10
Changed
Upgrade the Python container from Debian buster to bookworm and pin the Python version to 3.11.*.
2023-08-31
Changed
Upgrade to version 3 of the Compose file
Switch to using a named volume for node_modules.
Switch to always upgrade npm on build
2023-08-29
Changed
Changed to using a root user for local development. This fixes an issue that was happening where Vite and other JS
related tools where throwing write permission errors when running because the web service would create files as a
non-privileged app user and then JS tools would run as a non-privileged user and then try to write to
directories owned by root.
2023-06-01
Changed
Remove the Docker Compose volume for node for more consistent builds. This fixes the problem where sometimes you had
to run docker compose run node npm install after running docker compose build to install the node modules into the
local node volume. Instead the node modules are always installed into the docker image.
Fixed
Fixed Vite not being available after changes to package.json. This fixes #289
2023-04-01
Added
Add a pre_commit command to the justfile to run the lint, format, and test commands
2022-01-14
Changed
Removed the "Successfully signed in as" message after a user has signed in by add the ACCOUNT_SHOW_POST_LOGIN_MESSAGE
setting with it set to False by default.
By default, set ACCOUNT_EMAIL_VERIFICATION to "none" so that new hobby apps don't require transactional email set up.
Changed ACCOUNT_USERNAME_REQUIRED to False and ACCOUNT_AUTHENTICATION_METHOD to "email" so you can signup and signin
with just your email address.
Changed ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE to False for a nicer sign-up experience
Added
Added the adapter apps.accounts.auth_adapter.AccountAdapter to add the new custom settings
The ACCOUNT_SIGNUP_OPEN setting set it to false so signup is closed by default
Bash aliases and Django bash completion
2022-12-31
Added
A color picker to toggle between dark and light mode
Changed
Upgraded to Bootstrap 5.3.0-alpha1 in order to add the color picker
2022-12-27
Added
The packages django-test-plus and model-bakers
More tests
The upgrade_packages Just recipe
Just recipes for removing docker containers, images, volumes
Bandit for automatic security scanning
2022-12-26
Added
Just recipe, build_assets
Changed
Switch from the using docker-compose to using docker compose
Updated Django settings, so you can use config/settings/test_runner.py for pytest
Add the lock suffix to generated Python requirement files
Clean up and add more arguments to the start project script
2022-12-18
Added
Instructions on how to deploy to Fly.io
Changed
Make changes to settings to make it easier to deploy to platforms like Fly.io
2022-12-17
Changed
Switched the session backend from django-redis-sessions to the native django.contrib.sessions.backends.cache backend.
Switched from using django-redis-cache for parsing a REDIS_URL to using the native django.core.cache.backends.redis.RedisCache backend.
Move the vite asset tags to the bottom script block.
2022-12-12
Added
Missing accounts migration
Changed
Update the Dockerfile so it could be used for production builds
2022-12-11
Changed
Upgrade to Vite 4.0
2022-12-10
Changed
Upgrade to Django 4.1
Change python version to 3.11
Move the Javascript config files for eslint, stylelint, and Vite from the root directory to src/config
Change the mkdocs port from 5000 to 4000 since Airtunes/Airplay are taking that port
Move the mkdocs.yml config to the docs directory
Move the Docker files and requirement files under the config directory
Switch from using Flake8 to using Ruff
2022-12-08
Changed
Switch from using Make for common commands to Just