Configuration Surface and Defaults
A VUEDA project is configured in three places: the Django settings that core.default_settings.get_defaults builds, the query parameter names that the server and client both hold, and a few Vite environment variables on the client. This page describes what each one sets, which values a project must supply, and what fails when a value is missing or the two sides disagree. It also states the request transaction rule that these defaults put in place.
Server Settings from get_defaults
get_defaults(env) reads values through an env adapter and returns a dict of Django settings. The adapter is any object with the methods of the EnvLike protocol. VUEDA ships TomlEnv, which reads a mapping built by core.config.load_toml and checks the process environment first. A project's settings module applies the result with locals().update(get_defaults(env)) and overrides individual keys after that line. The server configuration reference lists every key that it sets, with its default. It also lists the settings that VUEDA code reads beyond them, with their local fallbacks.
Required keys
get_defaults reads these keys with no default, so the project must supply each one:
- Identity and hosts:
SECRET_KEY,SITE_NAME,TIME_ZONE,ALLOWED_HOSTS,AUTH_USER_MODEL. - Frontend:
FRONTEND_DOMAIN,FRONTEND_LOGIN_URL,CSRF_TRUSTED_ORIGINS,CORS_ALLOWED_ORIGINS. - Storage:
DATABASE_URL,CACHE_URL,DATABASE_BACKUP_DIR,STATIC_ROOT,STATICFILES_DIRS. - Email:
SUPPORT_EMAIL, andNO_REPLY_EMAIL, the sender for password reset, welcome, and two-factor email. - Mailgun, only when
EMAIL_BACKENDisanymail.backends.mailgun.EmailBackend:ANYMAIL_MAILGUN_API_KEY,ANYMAIL_MAILGUN_SENDER_DOMAIN,ANYMAIL_MAILGUN_WEBHOOK_SIGNING_KEY,ANYMAIL_WEBHOOK_SECRET.
CACHE_URL is required because sessions are stored in the cache. Configure the Cache and Sessions describes the backend choices.
VUEDA code also reads one setting that get_defaults never sets. The VDQ attachment serializer builds attachment URLs from VDQ_URL, so a project that serves VDQ attachments sets it itself.
Optional keys
Optional keys have defaults: Django: DEBUG is False, LANGUAGE_CODE is "en-us", MEDIA_ROOT is "/tmp/media", LOGS_FOLDER is ".", and EMAIL_BACKEND is Django's console backend.
By default, get_defaults puts the email backend in Django's deprecated Django: EMAIL_BACKEND setting, with EMAIL_TIMEOUT set to 5. These settings still work on Django 6.1, and packages that a project depends on may not yet support the replacement. Passing use_mailers=True puts the same backend in Django 6.1's Django: MAILERS setting instead, as {"default": {"BACKEND": ..., "OPTIONS": {"timeout": 5}}}. Before opting in, read Django's MAILERS migration guide and confirm that your email packages (for example django-anymail) support MAILERS.
Derived values
After reading keys, get_defaults computes these settings:
- Django: INSTALLED_APPS is
DJANGO_APPS, thenVUEDA_APPS, thenTHIRD_PARTY_APPS, thenLOCAL_APPS.get_defaultsappendspgtriggerandpghistoryto the third-party apps. VUEDA_APPSmust includevueda.history, because the migrations of every app with a tracked model depend on it. Without it,get_defaultsraisesImproperlyConfigured.- VuedaHistoryMiddleware goes into Django: MIDDLEWARE directly after Django's
AuthenticationMiddleware, since it readsrequest.user. The system checksvueda_history.W001andvueda_history.W002report a project that removes it or moves it before authentication. PGHISTORY_APPEND_ONLYisTrueandPGHISTORY_CREATED_AT_FUNCTIONis"clock_timestamp()". The shipped migrations write both into their trigger SQL, soget_defaultssets them unconditionally.- The
defaultdatabase getsATOMIC_REQUESTSset toTrueandCONN_MAX_AGEset to0. On a PostgreSQL-family engine it also gets theREPEATABLE READisolation level. - When
drf_spectacularis importable,get_defaultsadds it to the third-party apps, sets VUEDA's schema class inREST_FRAMEWORK, and addsSPECTACULAR_SETTINGS. Removing the package later removes these settings.
Production defaults
core.default_settings.get_production_defaults returns the Sentry settings and makes SENTRY_DSN required. Nothing in VUEDA or the project templates calls this function or initializes Sentry, so a production settings file does both.
Request Transactions
Every request to a view runs in one database transaction on the default database. get_defaults turns this on with ATOMIC_REQUESTS, and Django then wraps each view call in Django: atomic. The transaction commits when the view returns a response and rolls back when the view raises.
DRF views catch exceptions and turn them into responses, so without further help Django would commit writes made before the error. VUEDA's exception handler, core.exceptions.debug_stack_exception_handler, marks the transaction for rollback before it builds any response. This covers every error it answers:
- Validation, permission, and not-found errors.
- Exceptions that DRF does not handle, which become a
500response. - The
409thatgate_warningsraises during Warning Confirmation, so a withheld write keeps nothing it did before the gate.
A Dry Run request also rolls back, after the action body returns. Two cases still commit:
- A view that returns an error response directly, without raising, commits the writes it made, whatever the status code.
- Writes to a database without
ATOMIC_REQUESTScommit, because only databases with the flag set take part.get_defaultssets the flag on thedefaultentry it returns, so a project that replaces that entry or adds another database sets the flag itself.
Wire Query Parameter Namespace
The server and client use the same fixed query parameter names, called Wire Query Parameters. Server settings define each name, and the client holds a matching constant in utils/constants:
| Name | Purpose | Server setting | Client constant |
|---|---|---|---|
s | Search | REST_FRAMEWORK["SEARCH_PARAM"] | utils/constants.SEARCH_PARAM |
o | Ordering | REST_FRAMEWORK["ORDERING_PARAM"] | utils/constants.ORDERING_PARAM |
p | Page number | PAGE_QUERY_PARAM | utils/constants.PAGE_PARAM |
ps | Page size | PAGE_SIZE_QUERY_PARAM | utils/constants.PAGE_SIZE_PARAM |
e | Expand | REST_FLEX_FIELDS2["EXPAND_PARAM"] | utils/constants.EXPAND_PARAM |
f | Fields | REST_FLEX_FIELDS2["FIELDS_PARAM"] | utils/constants.FIELDS_PARAM |
om | Omit | REST_FLEX_FIELDS2["OMIT_PARAM"] | utils/constants.OMIT_PARAM |
ct | Column totals | COLUMN_TOTALS_PARAM | utils/constants.COLUMN_TOTALS_PARAM |
VUEDAPageNumberPagination reads PAGE_QUERY_PARAM and PAGE_SIZE_QUERY_PARAM. DRF's own defaults for search and ordering are search and ordering; DRF Ecosystem Compatibility Boundaries describes that departure.
The client does not discover these names at runtime. #301 tracks reporting them to the client. Until then, renaming one on the server requires the same change to the client constant.
The server builds its set of accepted query keys from the same settings. After a server-side rename, a client that still sends the old key gets a 400 "Invalid query parameter" response from list and retrieve on VuedaViewSet and VuedaReadOnlyViewSet endpoints. Query parameter validation describes which keys each endpoint accepts.
ct is the one name whose accepted values depend on the viewset. When drf-spectacular is installed, get_override_parameters documents it on each list operation whose viewset declares Column Totals, with that viewset's total names.
Permission Name Patch
VUEDA names permissions with its CRUD actions. The Permission Mapping setting, PERMISSION_NAMES_MAPPING, renames Django's actions. By default it maps add to create, change to update, and view to read. Importing core.patch_django from the project's settings applies it. The import patches four things:
- core.patch_django.get_permission_codename replaces Django's
get_permission_codenameand renames the action through the mapping. - core.patch_django.get_builtin_permissions replaces the function that Django uses to build a model's default permission rows, and renames each action the same way.
- Every model that does not declare its own Django: Options.default_permissions gets
listadded to itsdefault_permissions, third-party models included. This is where each model'slistpermission row comes from. - When the mapping is a reverse mapping (it maps to Django's names), the import rewrites ObjectPermissions.perms_map to check those names.
The two replacement functions read the mapping when called and cache it until Django's Django: setting_changed signal reports a change. The perms_map rewrite is decided once, when patch_django is imported. The import leaves DynamicObjectPermissions and WorkflowObjectPermissions alone, because they resolve their action through the mapping when each check runs.
Map Django and VUEDA Permission Names gives the import placement and the steps for changing the mapping. Permissions lists the setting's values.
Client Model Config
On the client, Model Config comes from Model Info and the overrides that a project passes to setConfig. Reactive Data Flow describes how storeModelConfig builds, caches, and clears configs. Contract-First Dynamic UI describes which layer wins when defaults and overrides disagree.
Client Environment Variables
Three Vite build-time variables configure the client:
VITE_CSRF_COOKIE_NAMEnames the cookie that the client reads, through getCSRFValue, to fill theX-CSRFTokenheader on writes. It must equal the server's Django: CSRF_COOKIE_NAME.get_defaultsleaves that setting at Django'scsrftoken, and the project templates set both names only in their development settings.VITE_DJANGO_CONNECTION_PORT, when set, is appended as a port towindow.location.hostnameto form utils/connectionHostname.connectionHostname. Development setups use it when Vite and Django listen on different ports.VITE_PACKAGE_VERSIONis the project's client version, which useVersion reports asprojectClientVersion. No VUEDA code sets it (#300). While it is unset,newClientAvailablestays false.
Failure Modes
Missing required key. TomlEnv raises KeyError("Missing config key: <key>") when get_defaults reads it, so the server fails at startup and names the key. load_toml returns an empty mapping for a missing file, so a missing config.local.toml shows up only as this error.
Invalid typed value. Values from the environment are strings, and TOML values arrive typed; the typed accessors convert both. A boolean accepts 1, true, t, yes, y, or on, and 0, false, f, no, n, or off, in any case. Any other value, such as "maybe", raises a ValueError that names the key. An integer or float accessor names the key only for an empty string. Any other bad string, such as "abc", raises Python's own ValueError, which does not name the key.
Environment overlay surprises. TomlEnv checks the process environment before the TOML mapping by default, because its prefer_env argument defaults to True. With a prefix, TomlEnv adds the prefix to the key for both lookups, so the TOML keys need it too.
Permission patch missing. Without the patch_django import, Django creates permission rows under its own names (add_*, change_*, view_*) and creates no list_* rows. ObjectPermissions still checks create_*, read_*, list_*, update_*, and delete_*, so every check against an assigned permission fails. The symptom is 403 responses that do not match the user's assigned permissions. When the mapping changes later, existing permission rows keep the codenames they were created with.
Reverse mapping set after the import. If a reverse mapping is set after patch_django is imported, ObjectPermissions.perms_map keeps the entries selected at import. Its HTTP method checks then disagree with the mapping in effect. DynamicObjectPermissions and WorkflowObjectPermissions are unaffected.
CSRF cookie name missing or wrong. When VITE_CSRF_COOKIE_NAME is unset or differs from CSRF_COOKIE_NAME, the client sends no valid token. The server's CSRF check then answers 403 to POST, PUT, PATCH, and DELETE requests. GET requests still work, so the application looks healthy until the first write.