Canonical Registration and Model Discovery
Canonical Registration is the boundary for discoverability in VUEDA. If a model is not registered, it does not exist to the metadata API or the client. There is no automatic discovery from installed apps, no ORM introspection, no implicit scanning of serializer definitions. Registration is the single, explicit act that makes a model visible to the framework.
Everything downstream depends on this boundary. Client routes, form generation, permission gating, and action availability: all of it requires the model to be present in the registry. This page explains what registration is as a state model, what each registration state controls, and where the boundary is enforced.
Registration Is the Discoverability Gate
Registration is the only mechanism by which a model becomes visible to VUEDA's metadata API. A registered model appears in Model Info responses. A model that is not registered does not, regardless of whether it has a serializer, a viewset, migrations, or data in the database. None of those things alone makes a model discoverable.
This is the system's architectural spine. The metadata universe is exactly the set of registered models. The client cannot discover models that the server has not registered, and the server will not advertise models that have not been explicitly enrolled. There is no configuration file that lists models, no decorator that auto-registers them, and no startup scan that finds them. Registration is a deliberate call made in the application code.
Registration States and Transitions
A model exists in exactly one of three registration states:
Unregistered: the model is invisible to model-info and the client. It may have a Django model class, migrations, database tables, and even serializers or viewsets defined in code, but none of that matters until registration occurs.
Serializer-only: the model is visible in model-info with field, expand, and permission metadata. It will have empty action, filter, and ordering metadata. This state exists to support metadata consumers that only need field shapes; for example, when the client needs to resolve field types for a related model referenced through an expand, but that related model does not need its own CRUDL surface. Expand metadata is provided to allow expansion through this model into other models that may be registered fully or serializer only. Both register and register_serializer only require a Meta.model on the canonical serializer, not inheritance from VuedaSerializer; a canonical serializer that skips VuedaSerializer still needs to inherit VuedaExpandableFieldsSerializerMixin directly for its expand metadata to be generated, otherwise model_expands is empty.
Fully registered (serializer + viewset): the model is visible with complete metadata, including fields, expands, actions, filters, ordering, and permissions. This is the state required for the client to generate a functional UI surface for the model, with routes, forms, and views.
Only certain transitions between states are valid:
- Unregistered to serializer-only, via
register_serializer. - Unregistered to fully registered, via
register. - Multiple registrations for a single model is illegal. Attempting to register a canonical serializer for an already-registered model fails at startup.
The canonical serializer is unique per model. Two Django apps cannot register different serializers for the same model. The system enforces this as a startup constraint: the error surfaces immediately when the application boots, not at runtime when a request happens to hit the conflict.
Viewset Presence and Metadata Completeness
The distinction between serializer-only and full registration is architecturally significant because it determines which sections of the metadata response exist.
Serializer-only registration produces a model-info entry containing field and expand schema (types, constraints, read-only markers, choice indicators) and permission codenames. This is enough for metadata consumers that need to understand the shape of a model's data, like resolving field types for related-model choice lookups, but it is not enough to generate a CRUDL surface. Without a viewset, there are no actions to advertise, no filter definitions to expose, and no ordering capabilities to declare. A serializer-only model can be used for an inline model that is saved along with its parent model and has its data loaded through expandable fields.
Full registration produces the complete metadata surface. Actions (CRUDL plus any extra actions defined on the viewset), filter definitions, and ordering capabilities are all derived from the viewset. The serializer alone cannot express these; they depend on viewset configuration, permission checks, and router integration that only exist when a viewset is present.
In practice, this means that if a model appears in model-info but the client cannot generate routes or forms for it, the first thing to check is whether the model was registered with a viewset or only with a serializer.
Registration Timing
Registration must occur after Django's app registry is ready. The registration functions resolve content types internally, which requires the app registry, content type framework, and all dependent models to be fully initialized.
Performing registration at import time or module scope risks content-type resolution errors and ordering-dependent import failures. The established pattern is to register in AppConfig.ready(), which guarantees that all prerequisites are satisfied:
class MyAppConfig(AppConfig):
def ready(self):
from vueda.info.registration import register
from vueda.info.registration import register_serializer
from .serializers import InlineExpandedModelSerializer
from .serializers import MyModelSerializer
from .viewsets import MyModelViewSet
register(MyModelSerializer, MyModelViewSet)
register_serializer(InlineExpandedModelSerializer)This pattern is consistent across VUEDA's own modules: vueda.vdq, vueda.user, and vueda.release all register their models in ready().
How Model-Info Uses the Registry
The model-info viewset does not perform ORM introspection or scan installed apps. Its list and detail endpoints are derived exclusively from the set of registered models. If the registry is empty, model-info returns an empty list. If a specific model is requested that is not in the registry, model-info returns a 404. 404s for unregistered models will be JSON, returned by the model-info viewset. 404s for nonexistent URLs never reach Django and will be HTML, served by whatever web server sits in front of it.
Client Discovery and the Trust Boundary
The client fully trusts the registration boundary. It does not probe for models beyond what model-info advertises, does not attempt to construct routes for models it has not seen in metadata, and does not retry failed lookups on its own.
When the client requests metadata for a model and receives a 404, it caches that failure. This prevents retry storms: if a model is not registered, repeated navigation attempts to that model do not result in additional server requests. However, it also means that if a model is registered on the server after the client has already cached a 404 for it, the client must be reloaded to discover the newly registered model.
The client does not distinguish between "unregistered" and "nonexistent." Both produce the same opaque failure: navigation to that model is blocked, and no forms or views are generated. From the client's perspective, a model either has metadata or it does not, and the reason for its absence is not surfaced.
Failure Modes
Serializer-only registration without a viewset leaves the model visible in model-info but without action, filter, or ordering metadata. The client can see the model's fields and generate inline forms via expandable fields, but cannot generate CRUDL routes for it. If the model needs CRUDL routes, actions, filters, or ordering metadata, then it must be registered with a viewset.
Registration at import time can cause content-type resolution failures or ordering-dependent import errors. These surface as startup crashes that may be difficult to diagnose because the error messages reference content types or models that appear to be correctly defined. The fix is always to move registration into AppConfig.ready().
Duplicate canonical serializers fail at startup. If there are multiple attempts to register a serializer as the canonical serializer for the same model, the second registration call raises an error.
Cached 404 errors on the client block discovery of models that are registered after the client has loaded. There is no automatic cache invalidation for this case; a page reload is required.
Registry accessor mutations have no effect. The registry's accessor functions return defensive copies. Code that retrieves a registration entry and modifies it will not change the actual registry state. The registry is effectively immutable after startup.
Relevant Implementation Surface
- Python:
- REST:
- JavaScript: