djangoplay-web / Apps / DjangoPlay — paystream (Main Django Project)
DocsDjangoPlay WebAppsDjangoPlay — paystream (Main Django Project)

DjangoPlay — paystream (Main Django Project)

paystream is not a domain app — it is the Django project itself. It owns:

23 min readApplies to v1.2.2
On this page ▾
  1. 1. Summary
  2. 2. Architecture
  3. 2.1 Directory map
  4. 2.2 Host/subdomain routing (hosts.py + urlconf/)
  5. 2.3 App startup (apps.py::PaystreamConfig.ready())
  6. 3. Settings System
  7. 3.1 Environment selection and secrets resolution
  8. 3.2 app_settings/ — one file per concern
  9. 3.3 INSTALLED_APPS (verbatim order, from app_settings/core.py)
  10. 3.4 Middleware order (app_settings/middleware.py)
  11. 3.5 Logging (app_settings/logging.py)
  12. 4. Custom Admin Site & Console (custom_site/)
  13. 4.1 PaystreamAdminSite (custom_site/admin_site.py)
  14. 4.2 Custom console views (custom_site/admin_console_views.py)
  15. 5. Security Infrastructure (security/)
  16. 5.1 Secrets encryption (crypto.py, encrypt_env.py, decrypt_env.py, generate_key.py, bootstrap_secrets.py)
  17. 5.2 Signup abuse scoring (security/infra/)
  18. 5.3 Cloudflare Turnstile (infra/turnstile_service.py)
  19. 6. Issue Tracker Integration (integrations/issuetracker/)
  20. 6.1 Startup (apps.py::IssueTrackerIntegrationConfig.ready())
  21. 6.2 Access control (access_control/permissions.py)
  22. 6.3 Visibility (services/visibility.py)
  23. 6.4 Cross-subdomain session bridging (views/ui/session.py)
  24. 6.5 Attachment downloads (views/downloads/attachment.py)
  25. 6.6 Domain-event bridging (signals.py)
  26. 7. Project-Level Services (services/)
  27. 7.1 Runtime bootstrap
  28. 7.2 Template context processors (services/context_processors/)
  29. 8. Third-Party Integrations & External Services
  30. 9. Quick Reference — Tracing a Request End to End

Doc generated inspecting every file under webapp/paystream/ (settings package, app_settings/ security/ including all fraud/abuse infra services, custom_site/, urlconf/, hosts.py, integrations/issuetracker/ — the full adapter layer, services/, celery.py, apps.py, asgi.py/wsgi.py), plus webapp/manage.py and the root pyproject.toml for how the project is actually bootstrapped and versioned.

1. Summary

  1. The settings package (settings/ + app_settings/) — a layered, environment-aware, secrets-encrypted configuration system that every other app's apps.py/AppConfig and every docs/system/apps/*.md doc in this series ultimately depends on.
  2. URL/host routing (urlconf/, hosts.py) — the top-level ROOT_URLCONF, plus django-hosts-based subdomain routing that gives DjangoPlay its three public surfaces: the main app, issues.<host>, and docs.<host>.
  3. The custom Django Admin Site and console (custom_site/) — PaystreamAdminSite, the single AdminSite instance every app's ModelAdmin registers against (via utilities.admin.mixins.BaseAdminPage), plus the view functions behind the custom /console/ UI that utilities.admin.app_builder/dashboard_metadata feed into.
  4. The Issue Tracker integration (integrations/issuetracker/) — DjangoPlay's adapter layer on top of the third-party genericissuetracker package: identity resolution, RBAC visibility, audit-event bridging, a protected attachment-download endpoint, and the UI/API views served on the issues. subdomain.
  5. Security infrastructure (security/) — Fernet-based secrets encryption/decryption for the whole project, plus a signup-fraud-scoring pipeline (AbuseIPDB, disposable-email detection, garbage-name heuristics, Cloudflare Turnstile) consumed by users' signup flow.
  6. Background job wiring (celery.py at both project root and app_settings/celery.py) and structured logging (app_settings/logging.py) for the whole platform.
  7. Small project-level services: runtime Site/SocialApp bootstrapping (services/), a wireless/connectivity check endpoint, and the template context processors that inject site/subdomain URLs into every page.

Every other app in this documentation series is, from paystream's point of view, an entry in INSTALLED_APPS and a set of include()d URLs. This doc is the one place that shows how they're actually wired together into one running Django project — INSTALLED_APPS, middleware order, the subdomain topology, and the settings layering that decides what "dev" vs "staging" vs "prod" actually means.


2. Architecture

2.1 Directory map

plaintext
paystream/
├── settings/                  Environment-layered Django settings (§3)
│   ├── base.py                 Assembles all app_settings/*.py into one namespace
│   ├── dev.py / staging.py / prod.py   Environment overrides (loaded via DJANGO_SETTINGS_MODULE)
│   └── validation.py           Fail-fast startup checks (SITE_PROTOCOL/HOST/PORT/SECRET_KEY)
├── app_settings/               23 single-concern settings modules (§3.2), imported by base.py
├── security/                   Fernet secrets encryption + signup-fraud infra services (§5)
├── custom_site/                PaystreamAdminSite + custom console admin views (§4)
├── urlconf/                    ROOT_URLCONF (default.py→base.py) + per-subdomain urlconfs (§2.2)
├── hosts.py                    django-hosts ROOT_HOSTCONF — issues./docs./default routing (§2.2)
├── integrations/issuetracker/  Adapter layer over the third-party genericissuetracker package (§6)
├── services/                   Runtime Site/SocialApp bootstrap + context processors (§7)
├── apps.py                     PaystreamConfig.ready() — startup hooks (§2.3)
├── celery.py                   Celery app instance (project root, imported by __init__.py-style entry points)
├── asgi.py / wsgi.py           ASGI/WSGI entrypoints
└── views/wireless_check.py     Trivial connectivity-check endpoint, mounted on both default and issues hosts

2.2 Host/subdomain routing (hosts.py + urlconf/)

DjangoPlay uses django-hosts (ROOT_HOSTCONF = "paystream.hosts", set in app_settings/core.py) to route by subdomain before Django's normal ROOT_URLCONF resolution:

python
host_patterns = patterns(
    "",
    host(r"issues", "paystream.urlconf.subdomains.issues", name="issues"),
    host(r"docs",   "paystream.urlconf.subdomains.docs",   name="docs"),
    host(r"",       "paystream.urlconf.default",           name="default"),
)
  • issues.<host> → urlconf/subdomains/issues.py — mounts the Issue Tracker's UI (integrations/issuetracker/views/ui) at the subdomain root, its API/downloads under api/, helpdesk's public support form, a cross-subdomain session-check/relogin pair (IssueSessionCheckView/IssueReLoginView — see §6.4), and the shared wireless_check endpoint.
  • docs.<host> → urlconf/subdomains/docs.py — serves a static documentation site from settings.DOCS_ROOT (a filesystem path, not a Django app) via django.views.static.serve, with a catch-all serve_docs view that falls back to view.html for client-side markdown routing (i.e., the docs site is a static SPA-like bundle, not server-rendered per-page).
  • default host ("") → urlconf/default.py → urlconf/base.py — the main application, documented app-by-app throughout this series; urlconf/base.py is where every domain app's include() lives (see the utilities app doc §2.5 for the full table of what's mounted where) plus the custom console/admin routes (§4).

MIDDLEWARE (app_settings/middleware.py) puts django_hosts.middleware.HostsRequestMiddleware first and HostsResponseMiddleware last, which is required for django-hosts to correctly rewrite {% host_url %} template tags and reverse URLs across subdomains.

SESSION_COOKIE_DOMAIN/CSRF_COOKIE_DOMAIN are both set to .{SITE_HOST} in every environment (dev.py/staging.py/prod.py) — this is what makes a single login session valid across the main app, issues., and docs. subdomains simultaneously (the actual mechanism the Issue Tracker's cross-subdomain session-check endpoints, §6.4, depend on).

2.3 App startup (apps.py::PaystreamConfig.ready())

Runs two independent, defensively-guarded startup routines:

  1. Asset generation — calls mailer.engine.asset_builder.build_logo_assets() inside a bare try/except; a failure here only logs a warning and does not block startup.
  2. Runtime Site/SocialApp bootstrap — but only if the django_site table already exists (checked via raw connection.introspection.table_names(), swallowing OperationalError/ProgrammingError — i.e., this is a no-op on a fresh, unmigrated database, which is what lets manage.py migrate itself run without apps.ready() crashing). If the table exists, calls services.runtime_site.ensure_runtime_site() (creates/reuses a django.contrib.sites.Site row matching SITE_HOST/SITE_PORT and pins settings.SITE_ID to it at runtime — notably mutating settings.SITE_ID after Django has already started, not something set once at import time) then services.socialapps.ensure_google_socialapp() (idempotently creates/updates exactly one allauth Google SocialApp, deleting duplicates if more than one exists, and hard-fails with RuntimeError if the email OAuth scope is ever missing from its config).

3. Settings System

3.1 Environment selection and secrets resolution

  • manage.py defaults DJANGO_SETTINGS_MODULE to paystream.settings.dev if not already set in the environment.
  • wsgi.py/asgi.py default it to paystream.settings instead — the package, whose __init__.py is deliberately empty (docstring: "Do NOT place Celery or settings logic here"). In a deployment where DJANGO_SETTINGS_MODULE isn't explicitly exported (e.g. to paystream.settings.prod), WSGI/ASGI boot would import an empty settings module with none of DEBUG/DATABASES/INSTALLED_APPS defined — see §9, point 1.
  • Secrets resolution (app_settings/common.py::get_decrypted_value) checks, in order: os.environ → project .env file (via django-environ, plaintext or Fernet-encrypted) → ~/.dplay/.secrets (a plaintext KEY=VALUE file, described as the "source of truth" for sensitive keys in authx.py's module docstring). Any value beginning with the literal string gAAAAA (the Fernet token prefix) is decrypted with ENCRYPTION_KEY (security/crypto.py, Fernet-based); ENCRYPTION_KEY itself is resolved the same env→secrets-file fallback chain and the process hard-fails at import time (ValueError) if it can't be found — same pattern for SECRET_KEY in app_settings/core.py. security/bootstrap_secrets.py::bootstrap_secrets() / common.py::load_all_decrypted_values() decrypts every gAAAAA…-prefixed value in os.environ in place, used by the Celery bootstrap path so worker processes don't need to re-resolve secrets per-task.
  • SITE_URL construction (app_settings/site.py::build_site_url) is deliberately strict: raises if protocol isn't exactly "http"/"https", and omits the port only when it's blank/80/443 — every environment file (dev.py/staging.py/prod.py) calls this to compute the canonical SITE_URL after resolving SITE_PROTOCOL/SITE_HOST/SITE_PORT.
  • settings/validation.py::validate_settings runs at the end of base.py and raises ImproperlyConfigured for an invalid SITE_PROTOCOL, missing SITE_HOST, non-numeric SITE_PORT, or missing SECRET_KEY — a fail-fast guard against half-configured environments.

3.2 app_settings/ — one file per concern

settings/base.py assembles the whole INSTALLED_APPS/middleware/etc. namespace via from paystream.app_settings.X import * for 20 modules, in this fixed order: ai, authx, cache, celery, common, core, database, drf_spectacular (imported non-wildcard, as SPECTACULAR_SETTINGS), email, genericissuetracker, jwt, link_expiry, logging, middleware, rest_framework, security, site, static_media, templates, werkzeug, then settings/validation.py, and finally (after SITE_PROTOCOL is resolved) allauth.py. public.py exists but is currently empty.

Module Owns
core.py SECRET_KEY resolution, the full INSTALLED_APPS list (§3.3), AUTH_USER_MODEL = "users.UserIdentity", ROOT_URLCONF/WSGI_APPLICATION/ASGI_APPLICATION, ROOT_HOSTCONF/DEFAULT_HOST
database.py Single postgresql DATABASES["default"] entry, fully secret-resolved (no SQLite fallback anywhere)
cache.py django_redis-backed CACHES["default"] (zlib compression, 200-connection pool, 60s ping interval) — this is the cache Django's own cache framework calls resolve to, distinct from the raw core.utils.redis_client several apps (utilities, mailer) talk to directly instead
celery.py (app_settings) Redis-backed broker/result-backend URL construction, CELERY_BEAT_SCHEDULE (currently exactly one entry — audit.tasks.cleanup_expired_audit_events, daily at 02:00), platform-conditional worker pool (solo on macOS, prefork elsewhere), and configure_celery_settings() (injects all CELERY_* globals into Django's settings namespace — called explicitly, wrapped in try/except-raise, at the end of base.py)
rest_framework.py Global DRF defaults: JWT + session auth, IsAuthenticated by default, UserRateThrottle (100/hr) plus the named scopes custom/custom_search/token that utilities.api.rate_limits implements, drf_spectacular as the schema class, a custom EXCEPTION_HANDLER (users.views.ui.errors.custom_401)
jwt.py SIMPLE_JWT — 2-hour access tokens, 1-day refresh (1-minute if "remember me" is off — see the inline comment, though as written both keys coexist: REFRESH_TOKEN_LIFETIME and REFRESH_TOKEN_LIFETIME_REMEMBER_ME, and only users app code, not shown here, decides which one is actually applied), rotation + blacklisting enabled
authx.py Configuration for authx-identity — an external microservice, not an installed Django app (§8)
allauth.py AUTHENTICATION_BACKENDS, all ACCOUNT_*/SOCIALACCOUNT_* settings, points at users' custom adapters (CustomAccountAdapter, CustomSocialAccountAdapter)
security.py Session/cookie defaults (1-hour SESSION_COOKIE_AGE, cached_db session engine), AUTH_PASSWORD_VALIDATORS, and the three-environment Cloudflare Turnstile key set (TURNSTILE_*_DEV/STAGING/PROD) plus ABUSEIPDB_API_KEY
email.py SMTP/console/dummy backend selection via EMAIL_MODE, the bug-report feature flag + URL allowlist (REPORT_BUG_ENABLED_ON_URLS — consumed by utilities.context_processors.report_bug), and EMAIL_FLOW_LIMITS loading with a three-tier fallback (env JSON → configs/email_flow_limits.json → hardcoded FALLBACK_EMAIL_FLOW_LIMITS), validated by validate_limits()
link_expiry.py Central day-based expiry constants for verification/reset/unsubscribe links
drf_spectacular.py SPECTACULAR_SETTINGS — title/description/version (hardcoded "1.2.0", independent of pyproject.toml's APP_VERSION — see §9), JWT bearer security scheme, pre/post-processing hooks (apidocs.hooks.* plus a local inject_servers hook that reads settings.OPENAPI_SERVERS, itself set per-environment in dev.py/prod.py)
genericissuetracker.py Settings for the third-party genericissuetracker package — page size, anonymous-reporting toggle, the identity-resolver dotted path (users.services.issuetracker_identity_resolver...), default permission class and transition policy (both pointing into paystream.integrations.issuetracker.access_control.permissions, §6.2), the internal-visibility allowed-roles list (CEO, DJGO, SSO), and a 5000-char comment length cap (below the DB's hard 10000-char cap, per the inline comment)
middleware.py The full MIDDLEWARE list (§3.4)
templates.py TEMPLATES["DIRS"] (frontend/templates), the context-processor list (four of which are paystream's own — §7.2), and a cached template loader
static_media.py STATIC_URL/STATIC_ROOT/STATICFILES_DIRS (ManifestStaticFilesStorage for hashed prod filenames), MEDIA_URL/MEDIA_ROOT, and — oddly placed here rather than in an admin-specific module — AUTO_LOAD_THRESHOLD = 500 (the changelist auto-load threshold utilities.admin.base_admin_page.BaseAdminPage.get_auto_load_threshold reads)
logging.py Full structured logging config (§3.5)
ai.py aicore app's provider-agnostic LLM config — xAI/OpenAI/custom(Ollama-compatible) switch, temperature/token/timeout/rate-limit knobs
werkzeug.py RUNSERVER_PLUS dev-only debugger config
common.py The secrets-resolution primitives described in §3.1, plus a grab-bag of directly-exported site-identity settings (SITE_NAME, SUPPORT_EMAIL/PHONE/LOCATION, GITHUB_URL/LINKEDIN_URL, SUPERUSER_EMAIL — default "redstar@djangoplay.org", the same identity referenced by three other independent hardcoded-admin checks across the platform, §9 point 3 — and hardcoded founder bio constants used somewhere in frontend), and APP_VERSION read directly from the repo's root pyproject.toml at import time

3.3 INSTALLED_APPS (verbatim order, from app_settings/core.py)

plaintext
mptt, dal, dal_select2, django_hosts,
django.contrib.{admin, auth, contenttypes, sessions, messages, staticfiles, sites, humanize},
django_extensions, drf_spectacular,
audit, policyengine, frontend, apidocs, paystream, fincore, devtools,
rest_framework, core, utilities, mailer, users, teamcentral, helpdesk,
locations, industries, entities, invoices, aicore,
allauth, allauth.account, allauth.socialaccount, allauth.socialaccount.providers.google,
rest_framework_simplejwt.token_blacklist, simple_history,
compressor,
genericissuetracker, paystream.integrations.issuetracker

This is the definitive list of every installed Django app in the project — useful as a cross-check against any other doc in this series that references "every app" or "all installed apps." Note mptt (tree-structured models — likely used by one of the domain apps for hierarchical data, e.g. locations' region/subregion/city hierarchy or a category tree, not independently verified in this pass), dal/dal_select2 (django-autocomplete-light — likely superseded in practice by utilities's own hand-rolled AJAX form-options/filter-options endpoints, §2.5 of the utilities doc, though both being installed simultaneously is worth confirming isn't redundant), and django_extensions (dev tooling, e.g. runserver_plus/shell_plus) are present but not analyzed in depth in this pass.

3.4 Middleware order (app_settings/middleware.py)

plaintext
django_hosts.HostsRequestMiddleware
core.middleware.request_id.RequestIDMiddleware
core.middleware.client_ip.ClientIPMiddleware
django.middleware.security.SecurityMiddleware
whitenoise.middleware.WhiteNoiseMiddleware
django.contrib.sessions.middleware.SessionMiddleware
django.middleware.common.CommonMiddleware
django.middleware.csrf.CsrfViewMiddleware
django.contrib.auth.middleware.AuthenticationMiddleware
django.contrib.messages.middleware.MessageMiddleware
allauth.account.middleware.AccountMiddleware
core.middleware.timezone.TimezoneMiddleware
core.execution_context.middleware.ExecutionContextMiddleware
django.middleware.clickjacking.XFrameOptionsMiddleware
simple_history.middleware.HistoryRequestMiddleware
core.middleware.template_syntax.TemplateSyntaxErrorLoggingMiddleware
core.middleware.api_request_logging.APIRequestLoggingMiddleware
django_hosts.HostsResponseMiddleware

There is no login-enforcement middleware anywhere in this list — AuthenticationMiddleware only attaches request.user, it doesn't require authentication. Every view in the project is responsible for its own access control (@login_required, DRF permission_classes, or nothing at all) — this is directly relevant to the unauthenticated-AJAX-endpoint finding documented in the utilities app doc §5, and to the Issue Tracker's deliberately-open read endpoints (§6.2 below).

3.5 Logging (app_settings/logging.py)

Structured (JSON, via pythonjsonlogger) file logging plus human-readable console output, built programmatically rather than as one static dict:

  • Formatters: ConsoleLogFormatter (human-readable) and StructuredLogFormatter (JSON; an include_traceback variant exists for .errors-suffixed loggers).
  • Filters: ErrorOnlyFilter/NonErrorFilter split console output between the console and console_errors handlers; RequestTimezoneFilter attaches the request's resolved timezone (via core.request_context.get_timezone) to every log record.
  • Per-app rotating file handlers are generated in a loop over LOG_FILES (5MB, 5 backups each) — but only for apps listed in AUTO_LOG_APPS: users, locations, industries, entities, invoices, frontend, apidocs, policyengine, mailer (+ mailer.events/mailer.errors/mailer.audit), audit, django, django_redis, data_sync. Apps not in this list — helpdesk, teamcentral, fincore, aicore, paystream itself, utilities, devtools, core, genericissuetracker, and the issue-tracker integration — get no dedicated log file and fall through to the root logger (console only) unless they configure their own logger elsewhere (not found in this pass). Worth confirming with the team whether this is intentional or an oversight, especially for paystream and core, which carry security-relevant code (signup fraud scoring, secrets handling).
  • ENABLE_ASYNC_LOGGING (env-flag, default off) swaps console+file handlers for a single QueueHandler+console_errors pair per logger — but the per-app file handler that was just built for that same app is not included in the async handler list, meaning turning on async logging silently stops writing to the per-app rotating files for every non-.errors logger (§9, point 5).

4. Custom Admin Site & Console (custom_site/)

4.1 PaystreamAdminSite (custom_site/admin_site.py)

The single admin_site = PaystreamAdminSite(name='console') instance that utilities.admin.mixins.decorators.AdminIconDecorator and every app's ModelAdmin registration ultimately targets instead of Django's default site. Notable overrides:

  • has_permission(request) = request.user.is_active and request.user.is_authenticated for non-POST requests (POST always passes, to let CSRF middleware handle the rejection instead). This omits Django's normal is_staff requirement entirely — any active, authenticated user clears the AdminSite gate; per-model access is still enforced downstream (Django has_view_permission/registry checks — see the utilities doc §2.3/§2.1), but the "must be is_staff to even reach the admin" layer that ships with Django by default does not exist in this project. Worth confirming this is intentional given how many other layers already gate access (§9, point 2).
  • index() and login() redirect to the custom /console/dashboard/ and account_login routes respectively rather than rendering Django's default admin index/login pages — the built-in AdminSite UI is effectively bypassed entirely in favor of the custom console (utilities.admin.app_builder/dashboard_metadata, admin_console_views.py below).
  • admin_view() wraps every admin view so a raised PermissionDenied renders errors/base.html (via permission_denied()) instead of Django's default 403 page, while explicitly letting already-formed 401/403 HTTP responses (e.g. CSRF failures) pass through untouched.
  • catch_all_view() and app_index() redirect Django admin's own built-in URL patterns (/admin/<url>, /admin/<app_label>/) toward the custom generic_404 and /console/apps/<app_label>/ respectively — i.e., Django's native admin URL surface is intentionally routed away from wherever possible, in favor of the parallel /console/ URL space.

4.2 Custom console views (custom_site/admin_console_views.py)

Three @login_required view functions — app_index_view, single_app_view, custom_changelist_view — that back the /console/apps/... URLs (mounted in urlconf/base.py, reverse-named admin_single_app/admin_custom_changelist, and the functions utilities.admin.app_builder.build_app_list()/dashboard_metadata.py build links against). Each layers the same sequence of checks:

  1. A hardcoded "users app" special case: if app_label == "users", access is denied outright to everyone whose username != "redstar" — a fourth, independent hardcoded-identity access-control mechanism, this time username-based, alongside utilities.services.is_admin.PlatformAdminService's two-email allowlist, configs/admin_registry.json's "users": {"enabled": false} entry, and common.py's SUPERUSER_EMAIL default (all four ultimately point at the same conceptual "founder/owner" account but are three structurally different checks against three different identity attributes — username, email-set membership, and a registry flag — see §9 point 3).
  2. Registry check (utilities.admin.app_registry.is_app_enabled_for_user) — same "redstar"-username bypass as above.
  3. Model resolution + Django-native permission check (admin_instance.has_view_permission(request)) — this is the actual Django Group/Permission layer (policyengine's output) being consulted, and per custom_changelist_view's own inline comment, is treated as the "source of truth" for whether the page renders, layered after the registry and username checks have already run.

custom_changelist_view additionally wraps admin_instance.changelist_view() in a bare try/except that prints a full traceback (traceback.print_exc()) before re-raising — a debug-oriented pattern that will dump full stack traces to stdout/stderr in production unless caught by the deployment's own error monitoring.


5. Security Infrastructure (security/)

5.1 Secrets encryption (crypto.py, encrypt_env.py, decrypt_env.py, generate_key.py, bootstrap_secrets.py)

Symmetric encryption via cryptography.fernet.Fernet. generate_key.py is a one-shot key-generation script; crypto.py exposes generate_encryption_key/encrypt_value/decrypt_value (the latter raising ValueError on InvalidToken — wrong key or corrupted ciphertext). encrypt_env.py/decrypt_env.py (354/353 lines — not fully enumerated line-by-line in this pass, but their role is confirmed by authx.py's docstring and common.py's resolution chain) are the operator-facing tooling for converting a plaintext ~/.dplay/.secrets file into the Fernet-encrypted .env values get_decrypted_value() reads at runtime. bootstrap_secrets.py is the single function (bootstrap_secrets()) that decrypts everything into os.environ in place before Celery/Django settings load — used specifically so Celery worker processes (which may not go through the same Django settings-import path as the web process) still get plaintext secrets.

5.2 Signup abuse scoring (security/infra/)

Four independent signal sources, composed by SignupAbuseService.analyze() into a single weighted score and an allow / challenge / block / hard_block verdict:

Service Signal Score weight
GarbageNameService Heuristic detection of nonsense/garbage first/last names +20
DisposableEmailService Domain membership in configs/disposable_domains.txt (loaded once, process-lifetime cache via a class-level LOADED flag) +30
AbuseIPDBService abuseipdb.com reputation lookup (abuseConfidenceScore >= 75), Redis/Django-cache-backed for 24h per IP to control API spend; fails open (returns not abusive on any request/parsing exception) +50
Datacenter-IP detection (inline in SignupAbuseService, reusing the AbuseIPDB response's isp/usage_type fields) Substring match against a hardcoded keyword set (amazon, aws, digitalocean, ovh, hetzner, linode, vultr, google cloud, microsoft azure) +40
Suspicious User-Agent (inline) Substring match against a hardcoded keyword set (python-requests, curl, wget, scrapy, aiohttp, httpclient, bot, crawler, spider) +25

Thresholds: score <= 30 → allow; <= 60 → challenge; <= 100 → block; > 100 → hard_block. This service is defined here but consumed by users' signup flow (not independently re-verified against users' own app doc in this pass — worth cross-checking that doc for exactly where in the signup pipeline analyze() is called and what each verdict actually does).

5.3 Cloudflare Turnstile (infra/turnstile_service.py)

Server-side CAPTCHA verification against Cloudflare's siteverify endpoint. Notably validates the returned hostname against settings.ALLOWED_TURNSTILE_HOSTNAMES (a different allowlist per environment — dev.py/staging.py/prod.py) as a second check on top of Cloudflare's own success flag — this defends against a token solved on/for a different site being replayed here. Fails closed on any exception (returns success: False), unlike AbuseIPDBService's fail-open behavior — an intentional (if unstated) asymmetry between the two: a CAPTCHA-verification outage blocks signups, while a reputation-lookup outage doesn't.


6. Issue Tracker Integration (integrations/issuetracker/)

This is DjangoPlay's own code — an adapter/integration layer wired on top of the separately-installed third-party genericissuetracker package (present as its own INSTALLED_APPS entry, own models, own settings.get_setting() config accessor, own Django signals). paystream.integrations.issuetracker is where DjangoPlay-specific identity, RBAC, audit, and UI concerns get bridged onto that generic package without modifying it directly.

6.1 Startup (apps.py::IssueTrackerIntegrationConfig.ready())

Two things, both wrapped in a blanket try/except (comment: "Avoid breaking app startup if DB isn't ready", so this silently no-ops on a fresh/unmigrated DB):

  1. Imports signals.py (registers the receivers below).
  2. Calls IssueLabelBootstrapService.ensure_labels_exist() (idempotent get_or_create of two canonical labels, bug-internal/bug-public, process-lifetime-cached via a class-level _bootstrapped flag) and registers the tracker's three models (Issue, IssueComment, IssueAttachment) into audit.lifecycle.registry.AUDIT_TRACKED_MODELS — i.e., this is where the issue tracker opts itself into the platform's audit system, done imperatively at startup rather than via static config (see the audit app doc for what AUDIT_TRACKED_MODELS membership actually triggers).

6.2 Access control (access_control/permissions.py)

Two classes, both configured into genericissuetracker's settings (app_settings/genericissuetracker.py) so the third-party package calls back into DjangoPlay-specific logic:

  • IssueTrackerAccessPermission (GENERIC_ISSUETRACKER_DEFAULT_PERMISSION_CLASSES) — all SAFE_METHODS (GET/HEAD/OPTIONS) are allowed unconditionally, including for anonymous users; write access requires either anonymous+POST+ALLOW_ANONYMOUS_REPORTING (bug reports from the public), or a fully validated identity (UnifiedLoginService.validate_user — the same login-policy service users uses elsewhere — plus an IdentityQueryService snapshot check for is_active/is_verified) and, if ISSUE_INTERNAL_ALLOWED_ROLES is non-empty, role membership. Superusers bypass the login-policy check but not the active/verified snapshot check.
  • IssueStateTransitionOwnerPolicy (GENERIC_ISSUETRACKER_TRANSITION_POLICY) — governs status-change transitions specifically: superuser bypass, then issue-owner bypass (issue.reporter_user_id == user_id), then role-based check against the same ISSUE_INTERNAL_ALLOWED_ROLES setting (CEO, DJGO, SSO) — reading the role off user.employmentprofile.role.code, the same teamcentral-owned field policyengine reads (see the policyengine app doc §5).

6.3 Visibility (services/visibility.py)

IssueVisibilityService is the queryset-level enforcement layer (separate from the permission classes above, which gate the request but not which rows come back): non-privileged identities (not superuser, not in ISSUE_INTERNAL_ALLOWED_ROLES) get every issue/comment/attachment queryset filtered to is_public=True (or issue__is_public=True for the child models). This is what makes the read-endpoints-open-to-everyone design in §6.2 safe in practice — an anonymous or low-privilege caller can hit any read endpoint, but will only ever see rows already marked public.

6.4 Cross-subdomain session bridging (views/ui/session.py)

IssueSessionCheckView (GET, polls whether the shared session — valid across the main app and issues. subdomain via the shared SESSION_COOKIE_DOMAIN, §2.2 — is still authenticated/unexpired) and IssueReLoginView (POST, re-authenticates by username-or-email + password without leaving the issues. subdomain) are both decorated @csrf_exempt. This is a deliberate accommodation for cross-subdomain JS polling/XHR (a same-origin CSRF token from the main app's session wouldn't otherwise be usable for XHR calls originating on issues.), but it means IssueReLoginView — a credential-checking login endpoint — has no CSRF protection and no visible rate-limiting/throttle in the view itself; brute-force protection, if any, would have to come from elsewhere (e.g. Django's AUTHENTICATION_BACKENDS/account-lockout logic in users, not verified in this pass). Worth confirming with the team.

6.5 Attachment downloads (views/downloads/attachment.py)

protected_attachment_download — resolves an IssueAttachment by its public number (not the PK), re-checks visibility via IssueVisibilityService.filter_attachment_queryset(...).exists() (masking a visibility failure as a generic Http404, not a 403, deliberately per the module's own docstring — "404 masking"), checks the file still exists on disk, logs the access, then streams it via FileResponse(..., as_attachment=True).

6.6 Domain-event bridging (signals.py)

Five receivers (issue_created, issue_updated, issue_deleted, issue_status_changed, issue_commented — all Django signals defined and fired by the third-party genericissuetracker package itself) each build a metadata dict and call core.events.helpers.emit_event(..., category=EVENT_CATEGORY_DATA_GOVERNANCE). Every receiver is wrapped in its own try/except Exception: logger.exception(...) — the module docstring states this is deliberate ("Never raises") so a failure in the platform's own event/audit pipeline can never break the underlying issue-tracker operation the user actually asked for.


7. Project-Level Services (services/)

7.1 Runtime bootstrap

runtime_site.py::ensure_runtime_site() / socialapps.py::ensure_google_socialapp() — both process-lifetime-idempotent (module-level _initialized flag), both called only from PaystreamConfig.ready() (§2.3). ensure_runtime_site computes a domain string from SITE_HOST(:SITE_PORT), raises if it would exceed the Site.domain field's 100-char DB limit, and reassigns settings.SITE_ID at runtime to whatever Site row it finds/creates — a pattern worth being aware of since SITE_ID is normally treated as a static setting. ensure_google_socialapp additionally hard-fails (RuntimeError) if the resulting SocialApp.settings config is ever missing the email OAuth scope, since that scope is required for allauth's SSO signup flow elsewhere in the platform.

7.2 Template context processors (services/context_processors/)

Four processors registered in TEMPLATES["OPTIONS"]["context_processors"] (app_settings/templates.py):

  • app_version — injects APP_VERSION (from pyproject.toml) into every template.
  • site_context — the most complex: derives CURRENT_SUBDOMAIN from the request's Host header, maps it to a HOME_URL via a hardcoded SUBDOMAIN_HOME_MAP (issues→issues_ui:list, docs→docs, else→console_dashboard), and spreads every configured subdomain (from utilities.admin.url_utils.get_all_subdomains() — cross-referenced in the utilities app doc §2.6/§4.6) into {NAME}_URL template variables. Also injects SITE_URL/SITE_NAME/WEBSITE_URL/AI_ENABLED.
  • global_urls — injects the DRF-Spectacular schema URL (apidocs:schema, degrading to "" on NoReverseMatch) and TURNSTILE_SITE_KEY.
  • admin_urls — injects console_dashboard_url.

8. Third-Party Integrations & External Services

This section consolidates every external dependency/service paystream itself configures or talks to directly (as opposed to being installed purely for another app's use):

Integration Nature Where configured/used
authx-identity External HTTP microservice, not a Django app — its own base URL (AUTHX_BASE_URL, default http://localhost:8100), its own Postgres database (AUTHX_DATABASE_URL* — explicitly noted in authx.py as "AuthX service only — not Django's DB"), its own JWT signing keypair (RS256, AUTHX_PUBLIC_KEY/AUTHX_PRIVATE_KEY), issuer/audience claims (https://auth.djangoplay.org / djangoplay), and a service-to-service token (AUTHX_SERVICE_TOKEN) for DjangoPlay→AuthX /internal/* calls. This is the "third-party identity" system referenced in the platform overview; paystream/app_settings/authx.py only configures the client side — the actual integration code (HTTP calls, JWT verification) lives in users, not paystream (see the users app doc for that half).
genericissuetracker Third-party installed Django app (pip package), with paystream.integrations.issuetracker as DjangoPlay's adapter (§6). Configuration lives in app_settings/genericissuetracker.py.
Google OAuth (via allauth) Standard allauth.socialaccount.providers.google; credentials resolved per-protocol (GOOGLE_CLIENT_ID_HTTPS/_HTTP) in settings/base.py before allauth.py is imported (order-sensitive, called out in an inline comment); runtime-bootstrapped as a singleton SocialApp row by services/socialapps.py.
Cloudflare Turnstile CAPTCHA, HTTP call to challenges.cloudflare.com, three independent key-pairs per environment (§3.2, §5.3).
AbuseIPDB IP reputation HTTP API, one key (ABUSEIPDB_API_KEY), Django-cache-backed (§5.2).
Redis Two independent roles: (1) Django's CACHES["default"] via django_redis (app_settings/cache.py), (2) Celery broker/result-backend (app_settings/celery.py). Same physical Redis config values (REDIS_HOST/PORT/PASSWORD/SSL) resolved independently in both files rather than shared from one source — worth checking they're kept in sync if Redis connection settings ever change (§9).
PostgreSQL Sole database backend (database.py) — no SQLite anywhere, including presumably for tests (not independently verified).
xAI / OpenAI / Ollama-compatible custom endpoint aicore app's LLM provider, configured here (ai.py) but implemented in aicore (see that app's doc).
WhiteNoise Static file serving middleware (whitenoise.middleware.WhiteNoiseMiddleware in MIDDLEWARE) — no separate static-file CDN/service configured in this pass.
django-hosts, django-mptt, django-autocomplete-light (dal/dal_select2), django-simple-history, django-compressor, drf-spectacular, djangorestframework-simplejwt, django-extensions, django-environ, cryptography, requests Installed third-party Python packages inferred directly from INSTALLED_APPS and import statements across this app — not an exhaustive dependency audit (that would require reading pyproject.toml's full dependency list, not done in this pass).

9. Quick Reference — Tracing a Request End to End

For a new engineer asking "what actually happens between a browser hitting https://issues.djangoplay.org/some/path and a response coming back":

  1. django-hosts' HostsRequestMiddleware (first in MIDDLEWARE) inspects the Host header against paystream.hosts.host_patterns, matches issues, and routes to paystream.urlconf.subdomains.issues for the rest of request handling (not urlconf/default.py).
  2. Standard middleware runs (security headers, WhiteNoise for static files, session — shared across subdomains via SESSION_COOKIE_DOMAIN, CSRF, auth — attaches request.user but doesn't require it, django-simple-history's HistoryRequestMiddleware for change attribution, request-ID/client-IP/timezone/execution-context middleware from core).
  3. urlconf/subdomains/issues.py resolves the path — e.g. into paystream.integrations.issuetracker.views.ui for a UI page, or paystream.integrations.issuetracker.urls under api/ for an API call.
  4. If it's an Issue Tracker API/UI view backed by genericissuetracker, the third-party package's own view logic runs, but permission-gates through IssueTrackerAccessPermission/IssueStateTransitionOwnerPolicy (§6.2) — both DjangoPlay-specific classes injected via GENERIC_ISSUETRACKER_* settings — and queryset-filters through IssueVisibilityService (§6.3).
  5. Any domain event the third-party package fires (issue_created, etc.) is picked up by paystream.integrations.issuetracker.signals and forwarded into core.events.helpers.emit_event for the platform's audit pipeline (§6.6) — failures here are swallowed, never surfacing to the original request.
  6. django-hosts' HostsResponseMiddleware (last in MIDDLEWARE) does any final host-aware response rewriting before the response is returned.

For the equivalent trace through the default host and the custom console/admin UI specifically, see §4.2 (the three-layer admin_console_views.py check sequence) and the utilities app doc §2.3/§7 (the registry/permission resolution those views call into).