Theme
Version
GitHub PyPI Discord
On this page

Events migration parity

Use this matrix before porting a component that depends on more than basic clicks, forms, State, and targeted renders. It distinguishes behavior that is available in current Events code from later candidates and features Citry intentionally leaves to explicit application code.

The source-specific walkthroughs are:

Delivery labels

LabelMeaning
v1Shipped and supported now.
v1.xA scoped follow-up release; the Citry answer column says whether it is shipped or planned.
v2A separate design decision, not a compatibility promise.
DroppedIntentionally represented another way or left to application code.

A dash in a source-framework column means that framework does not supply the capability as a first-class feature.

Handlers, routes, and return values

CapabilityComponent.Viewdjango-unicornTetralivecomponentsCitry answerDelivery
Callable declarationHTTP-verb methodMost public methods@public@commandPublic method inside nested Eventsv1
Explicit allowlistOverridden verbsPublic by default with exclusionsDecoratorDecoratorPlacement in Events; underscore methods are privatev1
Several actions per componentVerb or query multiplexingNamed methodsNamed methodsNamed commandsOne named handler per operationv1
HTTP-verb migrationNative model---ViewEvents for GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONSv1
TRACE compatibilitySupported by old view class---No compatibility handlerDropped
Custom route patternPer componentFixed message routeFixed call routeFixed command routeFixed per-event, batch, and compatibility routesDropped
Per-operation URLOne class URL--Command URL tagevents.url(name) / get_event_url(...)v1
Query and fragment URL buildingget_component_url--Query-built command URLquery= and fragment= on the URL buildersv1
Reverse args for custom pathsSupported---Stable fixed event path needs no reverse argsDropped
GET/read handlerVerb methodPOST message routePOST call routePOST command route@event(methods=("GET",)); author keeps it read-onlyv1
Raw host requestDjango requestDjango requestDjango requestDjango requestFramework-neutral request, with request.native escapev1
Async handlerHost-dependent-Async transport internals-Native under ASGI; rejected by sync hostsv1
Host portabilityDjangoDjangoDjangoDjangoDjango, FastAPI/Starlette, Flask, plain ASGI and WSGIv1
Typed inputManual parsingHint-driven call coercionJS argumentsBody kwargsOne strict data schema with field errorsv1
ORM-primary-key revival-AutomaticObject token stateStored model objectsLoad by validated id and authorize explicitlyDropped
JSON return to callerHost responseReturn valuePromise resultExecution-result modeldict or actions.Data resolves the caller promisev1
OpenAPI document----Deterministic OpenAPI 3.1 CLI outputv1
Served openapi.json----Planned HTTP document routev1.x
Native form without JavaScriptManualClient runtime expectedClient runtime expectedhtmx expectedPer-event URL with HTML, redirect, or JSON translationv1

ViewEvents preserves the method-selected route only. Its handler bodies still move to typed data, the neutral request, and Events return values. The method-only compatibility URL has no dedicated public builder; events.url("post") builds the named /post route.

State, serialization, and later renders

CapabilityComponent.Viewdjango-unicornTetralivecomponentsCitry answerDelivery
State declarationNonePublic class attributesPublic/private component attributesPydantic state modelExplicit dataclass-shaped State, separate from Kwargsv1
Default storageApplication-ownedClient data plus checksum and cached pickleEncrypted pickle tokenRedis pickleFull-HMAC signed strict JSON tokenv1
Server-held StateApplication-ownedCached component-Default_storage = "server" through Citry.cache, strict JSONv1
Optional encrypted StateApplication-owned-DefaultServer-heldOptional encrypted tokenv1.x
Browser visibility controlApplication-ownedjavascript_excludePublic/private splitServer-held_public controls projection, not secrecy; server storage keeps values out of the tokenv1
Client-writable StateApplication-ownedRich public settersPublic attributesCommands only_model selects writable public fieldsv1
Rich Python values in StateApplication-ownedModels and custom objectsPickled component graphPickled/Pydantic valuesStrict JSON onlyDropped
Per-component expiryApplication-ownedCache policyToken max agePage-session TTL_max_age on signed or server Statev1
Original kwargs on a callRequest code decidesRehydrated objectSaved componentStored State/contextHandler receives none; rebuild the tree explicitlyDropped
Original fills on a callRequest code decidesRe-rendered templateSaved componentSaved raw templateSupply fills again in the returned fresh treeDropped
Saved page contextRequest code decidesPartial framework contextSaved component contextSelected context in storeReload from request, database, or explicit StateDropped
Implicit re-renderNoYesYes by defaultDirty by defaultReturn a component or Render explicitly; None acknowledgesDropped
Stateless handlerNaturalFull component stateBasic component variantSpecial subclassOmit State; no token is mintedv1
Mutable undeclared server bagNaturalYesPrivate pickled attrsStored component StateOnly declared State survives the callDropped
Pluggable server storeApplication-ownedDjango cacheToken designStore and serializer classesConfigure Citry.cache; State remains strict JSONv1

Signed State is tamper-evident, not secret. State held on the server is the v1 choice when a value must not appear in the page token.

Bindings, Alpine, and DOM updates

CapabilityComponent.Viewdjango-unicornTetralivecomponentsCitry answerDelivery
Server event in markupHandwritten JS/htmxunicorn:<event>Alpine listener calling methodhx-post command@c-<dom-event>v1
Local browser eventHandwrittenHandwrittenOrdinary AlpineAlpine/htmxOrdinary @click / x-on:* stays Alpinev1
Event argumentsRequest fieldsPython-like call stringJS argumentshx-valsOne Alpine object expression validated as datav1
Python call expression on wire-Supported--Named handler plus object dataDropped
Property-setter expression-SupportedPublic Alpine write-Named handler or local $state writeDropped
State/model bindingHandwrittenunicorn:modelPublic Alpine attrsForm/htmx:c-field or :c-field="handler"v1
Nested dotted binding pathHandwrittenSupportedNested JS objectsBody valuesBind one top-level declared State fieldDropped
Prevent and stopBrowser/htmxModifiersAlpine modifiershtmx/Alpine.prevent, .stopv1
Self, once, key filtersBrowser/htmxPartialAlpine modifiersAlpine modifiers.self, .once, .enter, .escapev1
Debounce and throttleHandwrittenDebounceDecorator chainhtmx modifiersBinding modifiers and @event defaultsv1
Lazy/custom update eventHandwritten.lazyAlpinehtmx.lazy and .on:<event>v1
Deferred State writeHandwritten.deferClient statehtmx values$state writes and pending two-way updates ride the next callv1
Discard pending writesHandwritten.discard--Application decides which value to sendDropped
Loading UIHandwrittenLoading directivesLifecycle/Alpinehtmx indicator$loading() / $loading(name) and lifecycle eventsv1
Error UIHost responseError context/attrsMethod-error eventHTTP error$error, field map, and error lifecycle eventv1
Dirty-input indicatorHandwrittenunicorn:dirtyAlpinehtmxBuild from Alpine and lifecycle events when neededDropped
PollingHandwrittenRich poll objectApplication codehtmx@c-poll.<time>="handler", hidden-tab pausev1
Dynamic poll retimingHandwrittenPollUpdateApplication codehtmxRe-render a different binding or use app codeDropped
Viewport triggerHandwrittenunicorn:visibleAlpine/pluginhtmx triggerIntersectionObserver or Alpine integrationDropped
Morph opt-outManualunicorn:ignoreAlpine morph controlsno_morph helperBare #c-ignore on an inner plain elementv1
Stable item identityManual idsComponent keyComponent idPath id#c-key within its sibling and depth windowv1
Single element rootNot required by coreRequiredRequiredRoot attrs requiredSupportedv1
Multi-root componentSupported by core---Logical root groupv1
Text-only or empty componentSupported by core---Logical rootless range with lifecycle supportv1
Focus/value/caret preservationHandwrittenMorph-dependentAlpine morph rulesAlpine morphCompatible morph plus keyed identity and pending-write rulesv1
Self renderManual responseAutomatic full componentAutomaticDirty componentReturn component or Render(target=None)v1
Target another regionManual fragmentPartial/parent controlsClient callbackDirty resultOrdered Render(target=selector) actionsv1
Several target matchesManualPartials-OOB fragmentsOne logical mirrored instance with shared Statev1
Independent target instancesManualComponent instancesComponent instancesComponent idsOne Render action per targetv1
Parent/ancestor command APIRequest codeParent controlsChild-state graphCallContext.parent/find_oneExplicit target Render or Dispatch plus listenerDropped
Server-dispatched browser eventHandwrittenQueued JS call_dispatchTriggerEventsactions.Dispatch and DOM/onEvent listenerv1
Arbitrary server-selected JS callHandwrittenAllowed call listCallback pathhtmx eventsDispatch data; client-owned code chooses behaviorDropped
Browser call outside markupFetchUnicorn.callGenerated methodhtmx requestCitry.events.send, sendEvent, $sendEventv1
Python lifecycle matrixView hooksHydrate/update/call/render hooksComponent lifecycleCommand lifecycleEvents extension hooks, not per-field callback parityDropped
Browser lifecycle eventsHandwrittenFramework eventsTetra eventshtmx eventsbefore, after, error, swapped, and stale Events lifecyclev1
Fragment assets activateManual dependency strategyFramework runtimeResponse bundle listManual five-part client setupFragment carries dependency, graph, and Events manifestsv1
Client prerequisiteApplication choiceUnicorn JSTetra plus Alpinehtmx, json-enc, Alpine, morph, configCitry-managed runtimes with pinned stock Alpine and morphv1
Template-load validationLimitedMostly runtimeMostly runtimeMostly runtimeLiteral event, State, and modifier mistakes fail earlyv1
Slot/fill scope after updateApplication-ownedTemplate re-renderSaved componentSaved templateNew fills in the returned tree; Citry graph owns client scopev1

Citry still uses Alpine. The difference is automatic bundling and a Citry-owned logical component graph for scope, slots, multi-root groups, and rootless ranges. State remains under $state; it is not flattened into user x-data.

Queueing, transport, and stale responses

CapabilityComponent.Viewdjango-unicornTetralivecomponentsCitry answerDelivery
HTTP callsNative viewMessage POSTCall POSTCommand POSTFetch over batch or per-event routev1
Pending updates plus callsApplication codeAction queueFull public dataCommand bodyQueue batches co-eligible calls and pending State writesv1
Opt out of bundlingApplication code---@event(bundle=False)v1
Same-tick global coalescingApplication codeQueue-specific--Separate later queue optimizationv2
Stale-response defenseApplication codeEpochOld-value/focus checksSession StateEpoch plus optional latest_winsv1
Call orderingHost orderOptional serial cacheClient queueOne command, several dirty resultsSame-instance order, containment dependencies, sibling parallelismv1
Offline replay queueApplication code-Shipped-Application-specific retry and conflict policyDropped
Custom transportCustom view-HTTP/WS internalshtmxPublic transport registrationv1
postMessage bridgeCustom code---No built bridge yet; an explicit bridge is plannedv1.x
WebSocket and server pushCustom code-Reactive components-Signed-topic WebSocket design decisionv2
Server-sent eventsCustom code---Evaluated with the push decisionv2

Security, forms, files, and navigation

CapabilityComponent.Viewdjango-unicornTetralivecomponentsCitry answerDelivery
CSRFDjango middlewareDjango middlewareDjango middlewareDjango header setupSame-origin floor, host middleware, client token wiring, optional callablev1
Authorization hookHandler codeEndpoint plus handler codeHandler codeOpt-in decoratorComponent or handler guard plus handler checksv1
Custom payload codecHost code-Framework protocolJSON/form parsingRegistered payload codecv1
Custom return resolverHost responseFramework return handlingFramework callbacksExecution result classesRegistered event-result resolverv1
Typed form collectionManualModel bindingForm componentsBody kwargsNamed controls into typed datav1
Django form_class sugarManualShippedForm/ModelForm components-Convenience integrationv1.x
Field-error mapManualShippedform_errorsApplication codeSchema errors and EventError.fields in $error.fieldErrorsv1
Error attrs/template tagManualShippedTemplate stateApplication codeRender from $error explicitlyDropped
Multipart uploadRaw request filesLimitedShippedShippedBuilt-in multipart to UploadedFilev1.x
Staged multi-request uploadApplication code-Temporary filesUpload flowSingle-request multipart is the planned scopeDropped
HTTP file responseNative response-FileResponseApplication responseRouteResponse from @event(bundle=False) on a per-event HTTP callv1
File download actionHandwritten client-ShippedHeader resultShipped per-event actions.Download response; batches are rejectedv1.x
RedirectNative responseShippedShippedRedirectPageactions.Redirectv1
Push/replace historyHost responseRedirect metadataShippedShippedShipped PushUrl / ReplaceUrl actionsv1.x
Automatic unchanged-HTML 304Host codeShipped--Explicit no-action acknowledgementDropped
Automatic dirty-tree deduplicationHost codePartial logicSelf renderAncestor dirty-set dedupExplicit action order is authoritativeDropped

Setting csrf=False disables Citry's configurable token layer, whether it was automatic or callable. It does not disable the always-on same-origin/header floor or independently configured host middleware. State signatures also do not replace authorization: reload and authorize every record named by State or event data.

Events v1 acceptance checklist

The migration target is considered v1-complete when all of these remain true:

  • protocol examples replay against the Python dispatcher and validate against the protocol schema;
  • the same event suite passes under WSGI/Django and ASGI/FastAPI hosts;
  • host CSRF and per-event middleware are exercised without exempting the Events route;
  • the counter, debounced live search, and validated form pass through the real browser runtime;
  • the Component.View form and fragments ports pass through native and runtime paths, including fragment JavaScript, CSS, and Alpine activation;
  • State visibility, client-input, guard, and CSRF guidance is public; and
  • all four migration guides point to this maintained matrix.

For the API and authoring rules, continue with Server events and Security.