Theme
Version
GitHub PyPI Discord
On this page

Events migration parity

You are porting a component from another server-component library to Citry Events, Citry's server-events feature, and want to know: does it cover what my component relies on, and if not, what do I do instead? These tables answer that, one capability per row.

Clicks, forms, values kept between calls, and re-rendering part of the page all map directly. The biggest differences are deliberate: Citry does not re-render a component automatically after a call, does not keep the component's original inputs on the server, and stores only plain JSON between calls. File uploads, server push, and WebSockets are not built in.

For step-by-step ports, start from the guide for your library:

Read the tables

Each row names a capability, how each library provides it, what to use in Citry, and a delivery label:

LabelMeaning
v1Built into Citry Events today.
v1.xNot built in. The Events design lists it as a smaller addition after v1. Use the workaround in the Citry column today.
v2Not built in. It needs a larger design decision of its own, such as a new transport. Use the workaround in the Citry column, if any.
DroppedCitry does this a different way on purpose, or leaves it to your code. The Citry column says how.

The v1.x and v2 labels describe how big the missing piece is, not a release date. Plan your port around the workaround.

A dash in a library's column means that library has no built-in feature for it.

A few Citry terms appear throughout:

  • A handler is a public method in a component's nested class Events. Browser code can call it by name.
  • State holds the values a handler needs on the next call. Citry keeps them between calls, either on the server or in the page.
  • A per-event route is the URL of one handler. Calls made from templates are usually bundled and sent to a shared batch route instead.

Declare handlers

CapabilityComponent.Viewdjango-unicornTetralivecomponentsCitryDelivery
Callable declarationHTTP-verb methodMost public methods@public@commandPublic method in nested Eventsv1
Explicit allowlistOverridden verbsPublic by default, with exclusionsDecoratorDecoratorMethods in Events; underscore methods stay privatev1
Several actions per componentVerb or query multiplexingNamed methodsNamed methodsNamed commandsOne named handler per operationv1
Typed inputManual parsingCoerced from type hintsJS argumentsBody kwargsOne strict data schema, with field errorsv1
JSON return to callerHost responseReturn valuePromise resultExecution-result modeldict or actions.Data resolves the caller's promisev1
Raw host requestDjango requestDjango requestDjango requestDjango requestFramework-neutral request; request.native for the host objectv1
GET (read-only) handlerVerb methodPOST onlyPOST onlyPOST only@event(methods=("GET",)); you keep it read-onlyv1
Async handlerHost-dependent-Async transport internals-Native under ASGI; rejected by sync hostsv1
Web frameworksDjangoDjangoDjangoDjangoDjango, FastAPI/Starlette, Flask, plain ASGI and WSGIv1
Per-operation URLOne class URL--Command URL tagevents.url(name) / get_event_url(...)v1
Query and fragment in URLsget_component_url--Query-built command URLquery= and fragment= on the URL buildersv1
Plain HTML form without JavaScriptManualNeeds client runtimeNeeds client runtimeNeeds htmxPost to the per-event URL; get HTML, a redirect, or JSONv1
HTTP-verb handlersNative model---ViewEvents for GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONSv1
TRACE handlerSupported---No compatibility handlerDropped
Custom route patternPer componentFixed message routeFixed call routeFixed command routeFixed per-event and batch routes, plus the ViewEvents routeDropped
Reverse args for custom pathsSupported---Fixed event paths need no reverse argsDropped
Load model from primary key automatically-AutomaticObject token in stateStored model objectsLoad by validated id and check access yourselfDropped
OpenAPI document----Deterministic OpenAPI 3.1 from the CLIv1
Served openapi.json----Not served; write it with the CLI and serve the filev1.x

HTTP-verb handlers. ViewEvents keeps the route that picks a handler by HTTP method, so existing URLs keep working. The handler bodies still change: they read typed data and the neutral request, and return Events values instead of host responses. The method-only URL has no public URL builder; events.url("post") builds the named /post route instead.

Keep State across calls

CapabilityComponent.Viewdjango-unicornTetralivecomponentsCitryDelivery
State declarationNonePublic class attributesPublic/private attributesPydantic state modelExplicit dataclass-like State, separate from Kwargsv1
Default storageYour codeClient data, checksum, cached pickleEncrypted pickle tokenRedis pickleSigned strict-JSON token (full HMAC)v1
State kept on the serverYour codeCached component-Default_storage = "server" in Citry.cache, strict JSONv1
Stateless handlerNaturalFull component stateBasic component variantSpecial subclassOmit State; no token is createdv1
Control what the browser readsYour codejavascript_excludePublic/private splitServer-held_public limits what browser code reads; with signed State the other fields still travel in the tokenv1
Client-writable fieldsYour codePublic settersPublic attributesCommands only_model lists the writable public fieldsv1
Per-component expiryYour codeCache policyToken max agePage-session TTL_max_age on signed or server Statev1
Pluggable server storeYour codeDjango cacheToken designStore and serializer classesConfigure Citry.cache; State stays strict JSONv1
Encrypted State tokenYour code-DefaultServer-heldNot built in; use server State, with the field left out of _publicv1.x
Rich Python values in StateYour codeModels, custom objectsPickled componentPickled/Pydantic valuesStrict JSON onlyDropped
Undeclared attributes kept on the serverNaturalYesPrivate pickled attrsStored StateOnly declared State fields survive the callDropped
Original kwargs during a callRequest code decidesRehydrated objectSaved componentStored State/contextNot available; pass every input to the returned componentDropped
Original slot fills during a callRequest code decidesTemplate re-renderSaved componentSaved raw templatePass fills again in the returned componentDropped
Saved page contextRequest code decidesPartial framework contextSaved component contextSelected context in storeReload from the request, database, or StateDropped
Re-render after every callNoYesYes, by defaultDirty componentsReturn a component or Render; None re-renders nothingDropped

Signed State is readable by anyone

Signing lets Citry detect a changed token, but anyone can read the values in it. To keep a value out of the page, set _storage = "server" and leave the field out of _public.

Bind events

CapabilityComponent.Viewdjango-unicornTetralivecomponentsCitryDelivery
Server event in markupHandwritten JS/htmxunicorn:<event>Alpine listener calls methodhx-post command@c-<dom-event>v1
Browser-only eventHandwrittenHandwrittenAlpineAlpine/htmxVue @click / v-on:clickv1
Event argumentsRequest fieldsPython-like call stringJS argumentshx-valsOne Vue object expression, validated as datav1
Bind an input to StateHandwrittenunicorn:modelPublic Alpine attrsForm/htmx:c-field or :c-field="handler"v1
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 or custom update eventHandwritten.lazyAlpinehtmx.lazy and .on:<event>v1
Deferred State writeHandwritten.deferClient statehtmx values$state writes travel with the next callv1
Loading indicatorHandwrittenLoading directivesLifecycle/Alpinehtmx indicator$loading() / $loading(name) and lifecycle eventsv1
Error displayHost responseError context/attrsMethod-error eventHTTP error$error() / $error(name), field errors, error eventv1
PollingHandwrittenPoll objectYour codehtmx@c-poll.<time>="handler"; pauses in hidden tabsv1
Call from JavaScriptFetchUnicorn.callGenerated methodhtmx requestCitry.events.send, sendEvent, $sendEventv1
Mistakes caught at template loadLimitedMostly at runtimeMostly at runtimeMostly at runtimeWrong literal event, State, and modifier names fail earlyv1
Python call expression-Supported--Named handler plus object dataDropped
Property-setter expression-SupportedPublic Alpine write-Named handler, or a $state writeDropped
Nested dotted binding pathHandwrittenSupportedNested JS objectsBody valuesBind one top-level State fieldDropped
Discard pending writesHandwritten.discard--Your code decides which value to sendDropped
Dirty-input indicatorHandwrittenunicorn:dirtyAlpinehtmxBuild from Vue state and lifecycle eventsDropped
Change poll interval from serverHandwrittenPollUpdateYour codehtmxRe-render with a different binding, or use your own codeDropped
Run when scrolled into viewHandwrittenunicorn:visibleAlpine/pluginhtmx triggerIntersectionObserver or a Vue integrationDropped

Update the page

CapabilityComponent.Viewdjango-unicornTetralivecomponentsCitryDelivery
Re-render the calling componentManual responseAutomaticAutomaticDirty componentReturn the component, or Render(target=None)v1
Update another part of the pageManual fragmentPartial/parent controlsClient callbackDirty resultRender with a component or marker as targetv1
Update several parts at onceManualComponent instancesComponent instancesComponent idsOne Render per target, returned togetherv1
Stable list-item identityManual idsComponent keyComponent idPath id#c-keyv1
Keep focus and typed textHandwrittenDepends on morphAlpine morph rulesAlpine morphKept for reordered keyed rows and edited inputsv1
Keep DOM a JS library managesManualunicorn:ignoreAlpine morph controlsno_morph helper#c-ignore on an HTML elementv1
Send a browser event from the serverHandwrittenQueued JS call_dispatchTriggerEventsactions.Dispatch plus a listenerv1
Browser lifecycle eventsHandwrittenFramework eventsTetra eventshtmx eventsbefore, after, error, swapped, and stale eventsv1
Inserted HTML runs its JS and CSSYour dependency setupFramework runtimeResponse bundle listManual client setupLoads its assets and mounts its own Vue appv1
Client libraries to installYour choiceUnicorn JSTetra plus Alpinehtmx, json-enc, Alpine, morph, configNone; Citry loads its runtime with a pinned Vuev1
Component root shapeAnyOne elementOne elementOne element with root attrsOne element, several, text only, or emptyv1
Slot fills after an updateYour codeTemplate re-renderSaved componentSaved templatePass fills in the returned componentv1
One target, several matchesManualPartials-OOB fragmentsA target is one component or markerDropped
Call parent or ancestorRequest codeParent controlsChild-state graphCallContext.parent/find_oneRender with a target, or Dispatch plus a listenerDropped
Server picks a JS function to runHandwrittenAllowed call listCallback pathhtmx eventsDispatch data; browser code decides what to doDropped
Per-field Python lifecycle hooksView hooksHydrate/update/call/render hooksComponent lifecycleCommand lifecycleEvents extension hooksDropped
Skip unchanged HTML (304)Host codeBuilt in--Return None to acknowledge without renderingDropped
Merge duplicate re-rendersHost codePartial logicSelf renderAncestor dedupThe order of returned actions is what runsDropped

Update several parts at once. Put the Render actions next to each other in the returned list, one per target. The call fails if another action sits between them, if one uses delay or wait=False, or if two targets are the same or one contains the other. Nothing on the page changes then, although the handler has already run.

Keep focus and typed text. When keyed rows are reordered, the focused field keeps its focus, caret, and text. A text field keeps what the user typed until the server sends a different value.

Keep DOM a JS library manages. An HTML element with #c-ignore keeps its server-rendered contents through later renders. On a component tag, #c-ignore raises an error when the template loads.

Slot fills after an update. Each fill keeps the Vue scope of the template that wrote it.

Each Citry component with browser behavior is a Vue component, so Vue handles its slots, several root elements, and components that render no element. State lives under $state, separate from the component's own Vue data.

CapabilityComponent.Viewdjango-unicornTetralivecomponentsCitryDelivery
Typed form fieldsManualModel bindingForm componentsBody kwargsNamed controls into typed datav1
Field-error mapManualBuilt inform_errorsYour codeSchema errors and EventError.fields, in $error(name).fieldErrorsv1
RedirectNative responseBuilt inBuilt inRedirectPageactions.Redirectv1
Push or replace historyHost responseRedirect metadataBuilt inBuilt inactions.PushUrl / actions.ReplaceUrlv1
File downloadHandwritten client-Built inHeader resultactions.Download from a per-event callv1
Raw HTTP file responseNative response-FileResponseYour responseRouteResponse from an @event(bundle=False) handlerv1
Django form_class shortcutManualBuilt inForm/ModelForm components-Not built in; validate the form and raise EventErrorv1.x
File upload (multipart)Raw request filesLimitedBuilt inBuilt inNot parsed by default; a custom codec can pass UploadedFilev1.x
Upload across several requestsYour code-Temporary filesUpload flowNot built in; keep it in your codeDropped
Error attrs or template tagManualBuilt inTemplate stateYour codeRender from $error(name)Dropped

File download. actions.Download works only on a per-event call, so mark the handler @event(bundle=False). Return the download on its own, and do not change State in that handler. See Download a file.

File upload. Citry's built-in payload codecs do not read multipart/form-data. To accept files now, register a custom payload codec that turns the uploaded parts into UploadedFile values for the handler.

Secure event calls

CapabilityComponent.Viewdjango-unicornTetralivecomponentsCitryDelivery
CSRFDjango middlewareDjango middlewareDjango middlewareDjango header setupSame-origin check, host middleware, token sent by the client, optional callablev1
AuthorizationHandler codeEndpoint plus handler codeHandler codeOpt-in decoratorComponent or handler guard, plus checks in the handlerv1

Turning off the CSRF callable. csrf=False turns off only Citry's configurable token check. The same-origin check always runs, your host's CSRF middleware still runs, and the browser still sends its token.

Authorization. A signed State token proves Citry created it, not that the user may act on what it names. Load and check access for every record that State or event data names.

Send and queue calls

CapabilityComponent.Viewdjango-unicornTetralivecomponentsCitryDelivery
HTTP callsNative viewMessage POSTCall POSTCommand POSTFetch to the batch or per-event routev1
Send pending writes with a callYour codeAction queueAll public dataCommand bodyPending State writes travel with the next callv1
Call orderHost orderOptional serial cacheClient queueOne command at a timeOne Vue app sends its calls one at a time, in order; a wait: false call to an @event(detached=True) handler skips the linev1
Ignore stale responsesYour codeEpochOld-value/focus checksSession StateEpoch check, plus optional latest_winsv1
Send one call on its ownYour code---@event(bundle=False) uses the per-event routev1
Custom transportCustom view-HTTP/WS internalshtmxRegister a transportv1
Custom payload formatHost code-Framework protocolJSON/form parsingRegister a payload codecv1
Custom return handlingHost responseFramework return handlingFramework callbacksExecution result classesRegister an event-result resolverv1
postMessage bridgeCustom code---Not built in; register a transport that uses postMessagev1.x
Combine calls from the same tickYour codeQueue-specific--Not built in; each call is sent on its ownv2
WebSocket and server pushCustom code-Reactive components-Not built inv2
Server-sent eventsCustom code---Not built inv2
Offline replay queueYour code-Built in-Write your own retry and conflict rulesDropped

Events v1 acceptance checklist

The Citry project treats Events v1 as complete only while all of these hold. The items about hosts, CSRF, and the browser runtime are behavior your port can rely on; the rest describe how the project checks itself:

  • The protocol examples replay against the Python dispatcher and validate against the protocol schema, so the wire format matches its spec.
  • The same event test suite passes under WSGI/Django and ASGI/FastAPI, so a handler behaves the same on either host.
  • Host CSRF and per-event middleware run on event calls. You do not need to exempt the Events route.
  • The counter, debounced live search, and validated form examples pass through the real browser runtime.
  • The Component.View form and fragments ports pass through both plain HTML and the browser runtime, including the JavaScript, CSS, and Vue setup of inserted fragments.
  • The guidance on State visibility, client input, guards, and CSRF is public in Security.
  • All four migration guides link to this page.

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