DjangoPlay — paystream (Main Django Project)
paystream is not a domain app — it is the Django project itself. It owns:
On this page ▾
- 1. Summary
- 2. Architecture
- 2.1 Directory map
- 2.2 Host/subdomain routing (hosts.py + urlconf/)
- 2.3 App startup (apps.py::PaystreamConfig.ready())
- 3. Settings System
- 3.1 Environment selection and secrets resolution
- 3.2 app_settings/ — one file per concern
- 3.3 INSTALLED_APPS (verbatim order, from app_settings/core.py)
- 3.4 Middleware order (app_settings/middleware.py)
- 3.5 Logging (app_settings/logging.py)
- 4. Custom Admin Site & Console (custom_site/)
- 4.1 PaystreamAdminSite (custom_site/admin_site.py)
- 4.2 Custom console views (custom_site/admin_console_views.py)
- 5. Security Infrastructure (security/)
- 5.1 Secrets encryption (crypto.py, encrypt_env.py, decrypt_env.py, generate_key.py, bootstrap_secrets.py)
- 5.2 Signup abuse scoring (security/infra/)
- 5.3 Cloudflare Turnstile (infra/turnstile_service.py)
- 6. Issue Tracker Integration (integrations/issuetracker/)
- 6.1 Startup (apps.py::IssueTrackerIntegrationConfig.ready())
- 6.2 Access control (access_control/permissions.py)
- 6.3 Visibility (services/visibility.py)
- 6.4 Cross-subdomain session bridging (views/ui/session.py)
- 6.5 Attachment downloads (views/downloads/attachment.py)
- 6.6 Domain-event bridging (signals.py)
- 7. Project-Level Services (services/)
- 7.1 Runtime bootstrap
- 7.2 Template context processors (services/context_processors/)
- 8. Third-Party Integrations & External Services
- 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), pluswebapp/manage.pyand the rootpyproject.tomlfor how the project is actually bootstrapped and versioned.
1. Summary
- The settings package (
settings/+app_settings/) — a layered, environment-aware, secrets-encrypted configuration system that every other app'sapps.py/AppConfigand everydocs/system/apps/*.mddoc in this series ultimately depends on. - URL/host routing (
urlconf/,hosts.py) — the top-levelROOT_URLCONF, plusdjango-hosts-based subdomain routing that gives DjangoPlay its three public surfaces: the main app,issues.<host>, anddocs.<host>. - The custom Django Admin Site and console (
custom_site/) —PaystreamAdminSite, the singleAdminSiteinstance every app'sModelAdminregisters against (viautilities.admin.mixins.BaseAdminPage), plus the view functions behind the custom/console/UI thatutilities.admin.app_builder/dashboard_metadatafeed into. - The Issue Tracker integration (
integrations/issuetracker/) — DjangoPlay's adapter layer on top of the third-partygenericissuetrackerpackage: identity resolution, RBAC visibility, audit-event bridging, a protected attachment-download endpoint, and the UI/API views served on theissues.subdomain. - 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 byusers' signup flow. - Background job wiring (
celery.pyat both project root andapp_settings/celery.py) and structured logging (app_settings/logging.py) for the whole platform. - Small project-level services: runtime
Site/SocialAppbootstrapping (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
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 hosts2.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:
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 underapi/,helpdesk's public support form, a cross-subdomain session-check/relogin pair (IssueSessionCheckView/IssueReLoginView— see §6.4), and the sharedwireless_checkendpoint.docs.<host>→urlconf/subdomains/docs.py— serves a static documentation site fromsettings.DOCS_ROOT(a filesystem path, not a Django app) viadjango.views.static.serve, with a catch-allserve_docsview that falls back toview.htmlfor 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.pyis where every domain app'sinclude()lives (see theutilitiesapp 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:
- Asset generation — calls
mailer.engine.asset_builder.build_logo_assets()inside a baretry/except; a failure here only logs a warning and does not block startup. - Runtime
Site/SocialAppbootstrap — but only if thedjango_sitetable already exists (checked via rawconnection.introspection.table_names(), swallowingOperationalError/ProgrammingError— i.e., this is a no-op on a fresh, unmigrated database, which is what letsmanage.py migrateitself run withoutapps.ready()crashing). If the table exists, callsservices.runtime_site.ensure_runtime_site()(creates/reuses adjango.contrib.sites.Siterow matchingSITE_HOST/SITE_PORTand pinssettings.SITE_IDto it at runtime — notably mutatingsettings.SITE_IDafter Django has already started, not something set once at import time) thenservices.socialapps.ensure_google_socialapp()(idempotently creates/updates exactly oneallauthGoogleSocialApp, deleting duplicates if more than one exists, and hard-fails withRuntimeErrorif theemailOAuth scope is ever missing from its config).
3. Settings System
3.1 Environment selection and secrets resolution
manage.pydefaultsDJANGO_SETTINGS_MODULEtopaystream.settings.devif not already set in the environment.wsgi.py/asgi.pydefault it topaystream.settingsinstead — the package, whose__init__.pyis deliberately empty (docstring: "Do NOT place Celery or settings logic here"). In a deployment whereDJANGO_SETTINGS_MODULEisn't explicitly exported (e.g. topaystream.settings.prod), WSGI/ASGI boot would import an empty settings module with none ofDEBUG/DATABASES/INSTALLED_APPSdefined — see §9, point 1.- Secrets resolution (
app_settings/common.py::get_decrypted_value) checks, in order:os.environ→ project.envfile (viadjango-environ, plaintext or Fernet-encrypted) →~/.dplay/.secrets(a plaintext KEY=VALUE file, described as the "source of truth" for sensitive keys inauthx.py's module docstring). Any value beginning with the literal stringgAAAAA(the Fernet token prefix) is decrypted withENCRYPTION_KEY(security/crypto.py,Fernet-based);ENCRYPTION_KEYitself 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 forSECRET_KEYinapp_settings/core.py.security/bootstrap_secrets.py::bootstrap_secrets()/common.py::load_all_decrypted_values()decrypts everygAAAAA…-prefixed value inos.environin place, used by the Celery bootstrap path so worker processes don't need to re-resolve secrets per-task. SITE_URLconstruction (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 canonicalSITE_URLafter resolvingSITE_PROTOCOL/SITE_HOST/SITE_PORT.settings/validation.py::validate_settingsruns at the end ofbase.pyand raisesImproperlyConfiguredfor an invalidSITE_PROTOCOL, missingSITE_HOST, non-numericSITE_PORT, or missingSECRET_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)
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.issuetrackerThis 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)
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.HostsResponseMiddlewareThere 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) andStructuredLogFormatter(JSON; aninclude_tracebackvariant exists for.errors-suffixed loggers). - Filters:
ErrorOnlyFilter/NonErrorFiltersplit console output between theconsoleandconsole_errorshandlers;RequestTimezoneFilterattaches the request's resolved timezone (viacore.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 inAUTO_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,paystreamitself,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 forpaystreamandcore, which carry security-relevant code (signup fraud scoring, secrets handling). ENABLE_ASYNC_LOGGING(env-flag, default off) swapsconsole+file handlers for a singleQueueHandler+console_errorspair 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-.errorslogger (§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_authenticatedfor non-POST requests (POST always passes, to let CSRF middleware handle the rejection instead). This omits Django's normalis_staffrequirement entirely — any active, authenticated user clears theAdminSitegate; per-model access is still enforced downstream (Djangohas_view_permission/registry checks — see theutilitiesdoc §2.3/§2.1), but the "must beis_staffto 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()andlogin()redirect to the custom/console/dashboard/andaccount_loginroutes respectively rather than rendering Django's default admin index/login pages — the built-inAdminSiteUI is effectively bypassed entirely in favor of the custom console (utilities.admin.app_builder/dashboard_metadata,admin_console_views.pybelow).admin_view()wraps every admin view so a raisedPermissionDeniedrenderserrors/base.html(viapermission_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()andapp_index()redirect Django admin's own built-in URL patterns (/admin/<url>,/admin/<app_label>/) toward the customgeneric_404and/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:
- A hardcoded "users app" special case: if
app_label == "users", access is denied outright to everyone whoseusername != "redstar"— a fourth, independent hardcoded-identity access-control mechanism, this time username-based, alongsideutilities.services.is_admin.PlatformAdminService's two-email allowlist,configs/admin_registry.json's"users": {"enabled": false}entry, andcommon.py'sSUPERUSER_EMAILdefault (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). - Registry check (
utilities.admin.app_registry.is_app_enabled_for_user) — same"redstar"-username bypass as above. - Model resolution + Django-native permission check (
admin_instance.has_view_permission(request)) — this is the actual DjangoGroup/Permissionlayer (policyengine's output) being consulted, and percustom_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):
- Imports
signals.py(registers the receivers below). - Calls
IssueLabelBootstrapService.ensure_labels_exist()(idempotentget_or_createof two canonical labels,bug-internal/bug-public, process-lifetime-cached via a class-level_bootstrappedflag) and registers the tracker's three models (Issue,IssueComment,IssueAttachment) intoaudit.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 theauditapp doc for whatAUDIT_TRACKED_MODELSmembership 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) — allSAFE_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 serviceusersuses elsewhere — plus anIdentityQueryServicesnapshot check foris_active/is_verified) and, ifISSUE_INTERNAL_ALLOWED_ROLESis 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 sameISSUE_INTERNAL_ALLOWED_ROLESsetting (CEO,DJGO,SSO) — reading the role offuser.employmentprofile.role.code, the sameteamcentral-owned fieldpolicyenginereads (see thepolicyengineapp 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— injectsAPP_VERSION(frompyproject.toml) into every template.site_context— the most complex: derivesCURRENT_SUBDOMAINfrom the request'sHostheader, maps it to aHOME_URLvia a hardcodedSUBDOMAIN_HOME_MAP(issues→issues_ui:list,docs→docs, else→console_dashboard), and spreads every configured subdomain (fromutilities.admin.url_utils.get_all_subdomains()— cross-referenced in theutilitiesapp doc §2.6/§4.6) into{NAME}_URLtemplate variables. Also injectsSITE_URL/SITE_NAME/WEBSITE_URL/AI_ENABLED.global_urls— injects the DRF-Spectacular schema URL (apidocs:schema, degrading to""onNoReverseMatch) andTURNSTILE_SITE_KEY.admin_urls— injectsconsole_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":
django-hosts'HostsRequestMiddleware(first inMIDDLEWARE) inspects theHostheader againstpaystream.hosts.host_patterns, matchesissues, and routes topaystream.urlconf.subdomains.issuesfor the rest of request handling (noturlconf/default.py).- Standard middleware runs (security headers, WhiteNoise for static files, session — shared across subdomains via
SESSION_COOKIE_DOMAIN, CSRF, auth — attachesrequest.userbut doesn't require it,django-simple-history'sHistoryRequestMiddlewarefor change attribution, request-ID/client-IP/timezone/execution-context middleware fromcore). urlconf/subdomains/issues.pyresolves the path — e.g. intopaystream.integrations.issuetracker.views.uifor a UI page, orpaystream.integrations.issuetracker.urlsunderapi/for an API call.- If it's an Issue Tracker API/UI view backed by
genericissuetracker, the third-party package's own view logic runs, but permission-gates throughIssueTrackerAccessPermission/IssueStateTransitionOwnerPolicy(§6.2) — both DjangoPlay-specific classes injected viaGENERIC_ISSUETRACKER_*settings — and queryset-filters throughIssueVisibilityService(§6.3). - Any domain event the third-party package fires (
issue_created, etc.) is picked up bypaystream.integrations.issuetracker.signalsand forwarded intocore.events.helpers.emit_eventfor the platform's audit pipeline (§6.6) — failures here are swallowed, never surfacing to the original request. django-hosts'HostsResponseMiddleware(last inMIDDLEWARE) 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).