---
url: "https://vueda.dev/v3/core-concepts/configuration-surface-and-defaults.md"
title: "Configuration Surface and Defaults"
---

# Configuration Surface and Defaults

A VUEDA project is configured in three places: the Django settings that [core.default\_settings.get\_defaults](/v3/reference/api/py/vueda.core.default_settings.md#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](/v3/reference/api/py/vueda.core.default_settings/EnvLike.md) protocol. VUEDA ships [TomlEnv](/v3/reference/api/py/vueda.core.config/TomlEnv.md), which reads a mapping built by [core.config.load\_toml](/v3/reference/api/py/vueda.core.config.md#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](/v3/reference/configuration.md) 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`, and `NO_REPLY_EMAIL`, the sender for password reset, welcome, and two-factor email.
- Mailgun, only when `EMAIL_BACKEND` is `anymail.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](/v3/guides/configure-cache-and-sessions.md) 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](https://docs.djangoproject.com/en/6.1/ref/settings/#std-setting-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](https://docs.djangoproject.com/en/6.1/ref/settings/#std-setting-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`](/v3/reference/api/py/vueda.core.default_settings.md#get_defaults-param-use_mailers) puts the same backend in Django 6.1's [Django: MAILERS](https://docs.djangoproject.com/en/6.1/ref/settings/#std-setting-MAILERS) setting instead, as `{"default": {"BACKEND": ..., "OPTIONS": {"timeout": 5}}}`. Before opting in, read Django's [MAILERS migration guide](https://docs.djangoproject.com/en/6.1/howto/mailers-migration/) 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](https://docs.djangoproject.com/en/6.1/ref/settings/#std-setting-INSTALLED_APPS) is `DJANGO_APPS`, then `VUEDA_APPS`, then `THIRD_PARTY_APPS`, then `LOCAL_APPS`. `get_defaults` appends `pgtrigger` and `pghistory` to the third-party apps.
- `VUEDA_APPS` must include `vueda.history`, because the migrations of every app with a tracked model depend on it. Without it, `get_defaults` raises `ImproperlyConfigured`.
- [VuedaHistoryMiddleware](/v3/reference/api/py/vueda.history.middleware/VuedaHistoryMiddleware.md) goes into [Django: MIDDLEWARE](https://docs.djangoproject.com/en/6.1/ref/settings/#std-setting-MIDDLEWARE) directly after Django's `AuthenticationMiddleware`, since it reads `request.user`. The system checks `vueda_history.W001` and `vueda_history.W002` report a project that removes it or moves it before authentication.
- `PGHISTORY_APPEND_ONLY` is `True` and `PGHISTORY_CREATED_AT_FUNCTION` is `"clock_timestamp()"`. The shipped migrations write both into their trigger SQL, so `get_defaults` sets them unconditionally.
- The `default` database gets [`ATOMIC_REQUESTS`](https://docs.djangoproject.com/en/6.1/ref/settings/#std-setting-DATABASE-ATOMIC_REQUESTS) set to `True` and `CONN_MAX_AGE` set to `0`. On a PostgreSQL-family engine it also gets the `REPEATABLE READ` isolation level.
- When `drf_spectacular` is importable, `get_defaults` adds it to the third-party apps, sets VUEDA's schema class in `REST_FRAMEWORK`, and adds `SPECTACULAR_SETTINGS`. Removing the package later removes these settings.

### Production defaults

[core.default\_settings.get\_production\_defaults](/v3/reference/api/py/vueda.core.default_settings.md#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](https://docs.djangoproject.com/en/6.1/topics/db/transactions/#django.db.transaction.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](/v3/reference/api/py/vueda.core.exceptions.md#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 `500` response.
- The `409` that [`gate_warnings`](/v3/reference/api/py/vueda.core.exceptions.md#gate_warnings) raises during [Warning Confirmation](/v3/reference/glossary.md#warning-confirmation), so a withheld write keeps nothing it did before the gate.

A [Dry Run](/v3/reference/glossary.md#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_REQUESTS` commit, because only databases with the flag set take part. `get_defaults` sets the flag on the `default` entry 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](/v3/reference/glossary.md#wire-query-parameters). Server settings define each name, and the client holds a matching constant in [utils/constants](/v3/reference/api/js/utils/constants.md):

| Name | Purpose       | Server setting                      | Client constant                                                                 |
| ---- | ------------- | ----------------------------------- | ------------------------------------------------------------------------------- |
| `s`  | Search        | `REST_FRAMEWORK["SEARCH_PARAM"]`    | [utils/constants.SEARCH\_PARAM](/v3/reference/api/js/utils/constants.md#SEARCH_PARAM)        |
| `o`  | Ordering      | `REST_FRAMEWORK["ORDERING_PARAM"]`  | [utils/constants.ORDERING\_PARAM](/v3/reference/api/js/utils/constants.md#ORDERING_PARAM)      |
| `p`  | Page number   | `PAGE_QUERY_PARAM`                  | [utils/constants.PAGE\_PARAM](/v3/reference/api/js/utils/constants.md#PAGE_PARAM)          |
| `ps` | Page size     | `PAGE_SIZE_QUERY_PARAM`             | [utils/constants.PAGE\_SIZE\_PARAM](/v3/reference/api/js/utils/constants.md#PAGE_SIZE_PARAM)     |
| `e`  | Expand        | `REST_FLEX_FIELDS2["EXPAND_PARAM"]` | [utils/constants.EXPAND\_PARAM](/v3/reference/api/js/utils/constants.md#EXPAND_PARAM)        |
| `f`  | Fields        | `REST_FLEX_FIELDS2["FIELDS_PARAM"]` | [utils/constants.FIELDS\_PARAM](/v3/reference/api/js/utils/constants.md#FIELDS_PARAM)        |
| `om` | Omit          | `REST_FLEX_FIELDS2["OMIT_PARAM"]`   | [utils/constants.OMIT\_PARAM](/v3/reference/api/js/utils/constants.md#OMIT_PARAM)          |
| `ct` | Column totals | `COLUMN_TOTALS_PARAM`               | [utils/constants.COLUMN\_TOTALS\_PARAM](/v3/reference/api/js/utils/constants.md#COLUMN_TOTALS_PARAM) |

[VUEDAPageNumberPagination](/v3/reference/api/py/vueda.core.pagination/VUEDAPageNumberPagination.md) 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](/v3/core-concepts/drf-ecosystem-deviations.md) describes that departure.

The client does not discover these names at runtime. [#301](https://github.com/arrai-innovations/vueda/issues/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](/v3/reference/api/py/vueda.core.viewsets/VuedaViewSet.md) and [VuedaReadOnlyViewSet](/v3/reference/api/py/vueda.core.viewsets/VuedaReadOnlyViewSet.md) endpoints. [Query parameter validation](/v3/reference/glossary.md#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`](/v3/reference/api/py/vueda.core.open_api/VuedaBaseAutoSchema.md#get_override_parameters) documents it on each `list` operation whose viewset declares [Column Totals](/v3/reference/glossary.md#column-totals), with that viewset's total names.

## Permission Name Patch

VUEDA names permissions with its [CRUD](/v3/reference/glossary.md#crud) actions. The [Permission Mapping](/v3/reference/glossary.md#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](/v3/reference/api/py/vueda.core.patch_django.md) from the project's settings applies it. The import patches four things:

- [core.patch\_django.get\_permission\_codename](/v3/reference/api/py/vueda.core.patch_django.md#get_permission_codename) replaces Django's `get_permission_codename` and renames the action through the mapping.
- [core.patch\_django.get\_builtin\_permissions](/v3/reference/api/py/vueda.core.patch_django.md#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](https://docs.djangoproject.com/en/6.1/ref/models/options/#django.db.models.Options.default_permissions) gets `list` added to its `default_permissions`, third-party models included. This is where each model's `list` permission row comes from.
- When the mapping is a reverse mapping (it maps to Django's names), the import rewrites [ObjectPermissions.perms\_map](/v3/reference/api/py/vueda.core.permissions/ObjectPermissions.md#perms_map) to check those names.

The two replacement functions read the mapping when called and cache it until Django's [Django: setting\_changed](https://docs.djangoproject.com/en/6.1/ref/signals/#django.test.signals.setting_changed) signal reports a change. The `perms_map` rewrite is decided once, when `patch_django` is imported. The import leaves [DynamicObjectPermissions](/v3/reference/api/py/vueda.core.permissions/DynamicObjectPermissions.md) and [WorkflowObjectPermissions](/v3/reference/api/py/vueda.workflow.permissions/WorkflowObjectPermissions.md) alone, because they resolve their action through the mapping when each check runs.

[Map Django and VUEDA Permission Names](/v3/guides/permission-name-mapping.md) gives the import placement and the steps for changing the mapping. [Permissions](/v3/reference/permissions.md#permission-name-mapping) lists the setting's values.

## Client Model Config

On the client, [Model Config](/v3/reference/glossary.md#model-config) comes from [Model Info](/v3/reference/glossary.md#model-info) and the overrides that a project passes to [`setConfig`](/v3/reference/api/js/stores/storeModelConfig/methods/setConfig.md). [Reactive Data Flow](/v3/core-concepts/reactive-data-flow.md) describes how [storeModelConfig](/v3/reference/api/js/stores/storeModelConfig/functions/storeModelConfig.md) builds, caches, and clears configs. [Contract-First Dynamic UI](/v3/core-concepts/contract-first-dynamic-ui.md#configuration-precedence) describes which layer wins when defaults and overrides disagree.

## Client Environment Variables

Three Vite build-time variables configure the client:

- `VITE_CSRF_COOKIE_NAME` names the cookie that the client reads, through [getCSRFValue](/v3/reference/api/js/utils/csrf/functions/getCSRFValue.md), to fill the `X-CSRFToken` header on writes. It must equal the server's [Django: CSRF\_COOKIE\_NAME](https://docs.djangoproject.com/en/6.1/ref/settings/#std-setting-CSRF_COOKIE_NAME). `get_defaults` leaves that setting at Django's `csrftoken`, and the project templates set both names only in their development settings.
- `VITE_DJANGO_CONNECTION_PORT`, when set, is appended as a port to `window.location.hostname` to form [utils/connectionHostname.connectionHostname](/v3/reference/api/js/utils/connectionHostname.md#connectionHostname). Development setups use it when Vite and Django listen on different ports.
- `VITE_PACKAGE_VERSION` is the project's client version, which [useVersion](/v3/reference/api/js/use/useVersion/functions/useVersion.md) reports as `projectClientVersion`. No VUEDA code sets it ([#300](https://github.com/arrai-innovations/vueda/issues/300)). While it is unset, `newClientAvailable` stays 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.
