Server — decisions¶
Version: 0.6 · Last Updated: 2026-09-08 · Status: 🔴 DA REVISIONARE
The ground floor, with the work finished. Read this document as a report
from the day everything described here is running: it says what the server
is, in the present tense, and never what it lacks. What the code holds at
any given moment is status.md's subject, and the road between the
two is written one step at a time under steps/.
Every voice carries its source. A voice sourced to the owner and a date was decided in conversation before it reached any register.
The open frictions are the closing section, and they are the one place here that compares this arrival with the present — because settling them is what lets this document be ratified. When that section is empty, the design stands on its own.
1. The principle: static only where dynamic cannot be had¶
Source: D32 (ratified 2026-08-25; owner, 2026-08-23). Staticity is never a presupposition and never a goal. It is accepted where we have not managed to make something dynamic — and where it is accepted, the reason is written down. A design voice may not celebrate a fixed set, a boot-time-only decision or a restart-to-change behaviour as if immobility were a virtue.
This principle governs the rest of this document and every other entry: it is D32 in the specification log, and 00 overview states it where entries are written.
2. The base owns a small, closed list¶
Source: D2, SPECIFICATION.md:42. The common substrate of every server:
one uvicorn loop, one monitored thread pool for blocking work (async handlers
never touch it), the applications, the ordered lifespan, the request registry,
and authenticate()/session() answering "nobody / none".
Closed means this list and no more: a capability that is not on it is a mixin, not a member of the base. It does not mean immobile — what the base owns is fixed, what it holds is not (§4).
Source: D17, SPECIFICATION.md:229. The channel clause of D2 is amended: the base is born WITHOUT channels, and communication is the first capability mixin. This is what makes the list closed in practice rather than in principle.
Source: D2, SPECIFICATION.md:45. Serving is implemented once — recorded against the old repository, where it existed twice, written differently.
3. An application is a triplet, and its identity is stable¶
Source: commit a1a8f7e, 2026-07-25. An application is
code + instance + mount. code names it and is the key everything else
refers to — the configuration path applications.<code>., the monitor entry,
the default name. mount is the URL prefix, and mount="" IS the site
root: a deliberate value, so every default check is is None and never
truthiness, which would silently move a root application to /<code>.
Both are class attributes a subclass sets declaratively and a constructor kwarg overrides per instance, so one class can be served twice under two codes.
Source: owner, 2026-08-23. The identity is stable for the life of the instance, which is not the life of the process: an instance may be mounted and unmounted (§4) without its code ever meaning something else.
4. The installed set changes while the server runs¶
Source: owner, 2026-08-23; direction parked by D23,
SPECIFICATION.md:418-419 ("the two-stage live-config architecture — config
as live object, apply_configuration, hot/cold changes — stays parked as a
future macro").
Which applications are installed is a fact of the site configuration, and the site configuration is a live document. Mounting an application, unmounting one, moving one to a different prefix: each is an operation an administrator performs on a running server, never a reason to restart it.
The mechanism is the configuration itself:
- The site configuration is a Bag — a tree that is read, written, and that notifies whoever watches it.
- The
_serverapplication carries the commands that write into it: add an application entry, remove one, change one. The command surface is a section of that application and belongs to 090 server-application. - The write fires the Bag's own trigger, and the server — subscribed to the branch that lists the applications — brings the installed set in line with what the tree now says.
The command edits a document; the running system follows. Nothing is materialized from outside, in keeping with §12: the class that needs the values reads them, and here it also watches them.
A change that cannot be honoured is refused loudly. Two applications
claiming one code, two claiming one mount, a default naming an
application that is not installed: each is answered with an error the
administrator reads. A collision is never absorbed into a silent misroute
that surfaces later as a request arriving in the wrong place.
An application declares what may be done to it. Being installed or removed while the server runs is not something the server may assume: an application that holds live state — users, pages, open connections — says whether it can be taken away and put back, and by saying so it guarantees the mechanism that makes that possible. One that cannot does not claim it can, and then the change waits for a restart. Equally, an application may declare that a failure of its own mount is survivable, so the server starts without it rather than refusing to start.
Accepting a change and completing it are two different moments. The change is taken on atomically — it lands or it does not. What follows may take time: an application with people using it warns them, waits for them to finish, and lets go only then. So a change reports itself as accepted and in progress, and an administrator is never told "refused" while something was in fact removed. The mechanism is 015 configuration's; what each application does when its own entry changes is its own.
The same shape at every level. A command changes the configuration, the trigger fires, the thing adapts — for the SPA groups and their worker processes exactly as for the applications here. The groups' own design is 020 orchestration's.
Site configuration is not plugins. Two different mechanisms at two
levels, never to be folded together: the site configuration declares which
applications, databases and groups exist (the applications, databases,
commander sections of the tree); a plugin is a genro-routes router
plugin armed onto the router of one routed application, and is
025 routing system's subject. Installing an application is a
configuration change; enabling a plugin is a router change.
5. One demux rule¶
Source: D3, SPECIFICATION.md:61. First path segment → secondary mount if
it exists, otherwise primary app. One rule for every server. Mono-app is a
usage, not a mechanism: the internal server is a base server used with only
one application. This explicitly kills the old dual get_app semantics
(finding C5), and it "must never be reintroduced by package layering".
Source: commit a1a8f7e, 2026-07-25. The rule stays single and grows
fallbacks: first segment → that mount with the segment stripped; else the
application on the site root; else, for / with a default declared, a
307 to that application's mount preserving the query string; else 404.
307 and not 301/302 so that method and body survive the hop. A server with
nothing on the root is a legitimate shape, and default=<code> elects
nothing — it is a redirect target and no more.
Source: D29, owner, 2026-08-24. A bare / on a server with no root
application and no default serves the site index: an HTML page the
server itself builds, with the genro-asgi logo and links to the mounted
applications, excluding codes starting with _. A configuration switch
disables it; disabled, / answers 404. Deep unmatched paths are always 404.
6. Two servers, one base, distinguished by composition¶
Source: D1, SPECIFICATION.md:36. The official pair is public server
and internal server. The public server is the exposed face and owns auth,
sessions and origin gates. The internal server is never exposed: auth and
sessions are None by design, because whoever fronts it owns them.
Source: D6, SPECIFICATION.md:89. The internal server has no auth by construction — a class property, never a configuration. A wrong configuration must not be able to arm auth on a process that must never be exposed.
Source: D17, SPECIFICATION.md:229. Public server = base + communication +
auth + …; internal server = base + communication. And the sub-commander is
the public server class with parent= armed — no new class.
Source: D19, SPECIFICATION.md:263. Four usage levels, each usable on its
own: bare base server (embed an application, no channels) → public server
(config, auth, _server) → orchestration (groups, SPA, batch) →
multi-machine hierarchy. A consumer enters at the level they need and extends
with the same gesture the framework itself is built with.
7. The registry: one mechanism, two duties, different consumers¶
Historical source: D5 as amended by D30, owner, 2026-08-24. D30 gave the
registry two duties: expose the current request and track in-flight requests.
Subsequent restriction: D31 reserves registry access to framework machinery
and describes a thin registry item, not a handler's live Request. The later
N37 instruction (2026-09-06) explicitly approved opt-in _request injection in
RoutedApplication.bind_kwargs; it does not make the registry itself a public
handler API. The remaining reconciliation with the broader effects target is
recorded in Applications decisions.
Construction is not among them: the server dispatches the raw ASGI scope
and the owning application builds the Request its handlers receive — each
surface builds the request it needs. The websocket branch, when it reopens,
follows the same rule: the socket's owner (the channel) builds its object, and
the registry may register it as an in-flight item — register, never construct.
What differs between servers is the consumer of the picture: the monitor
on the public server, the occupancy sensor on the internal one.
Source: D5, SPECIFICATION.md:80. One mechanism only for "current request", owned by the registry instance. The old repository tracked it twice in parallel — the registry's ContextVar and a module-level global — and the module-level global goes away.
Source: D5, SPECIFICATION.md:86. The ledger of forwarded requests (the commander's QUIESCE bookkeeping) is a different thing with a different name: it tracks work the server did NOT execute locally, and belongs to the commander application, never to the base. See 020 orchestration.
Source: D18, SPECIFICATION.md:249. __slots__ only on high-cardinality
objects — requests, register items, events/frames, config nodes — and NEVER on
servers, managers, commanders or applications. The old repository had it
backwards (a 21-slot singleton server).
8. Requests carry their own end of life¶
Source: docstring of RequestRegistry, and its one consumer at
request.py:257. Whatever a request opened, the request closes: code holding
the current item queues a zero-argument callback, and the server drains them
at the end of the dispatch, whether the handler returned or raised. The
server never learns what the resource was: a database connection closing
itself is the plain case.
(Recorded as a docstring source, not a decision: it describes a mechanism
whose ratification was not found. See the friction on the error argument.)
9. The application contract is the deliverable¶
Source: D7, SPECIFICATION.md:93. What the server requires of an
application — ASGI callable, its identity, its server property, lifecycle
hooks — is born before any real application class and is exercised by a
throwaway test application with one sync route, one async route and one that
raises. The tests ARE the definition of the contract. For WebSocket, only
the socket in __call__ is present, and it is empty.
Source: D16, SPECIFICATION.md:217. Extension is subclassing, made real by
contract: every class peels its own kwargs and forwards the rest, mixins go
BEFORE the base in the MRO, and the end of the chain raises TypeError
naming any leftover.
Ownership, one direction. The server assigns app.server = self at
registration and the application-side setter accepts it once; the server
writes, the application reads. What "once" should mean when an application can
be unmounted and remounted (§4) is an open friction.
10. Lifespan: in order, in reverse, isolated¶
Source: D2, SPECIFICATION.md:42, and the Lifespan docstring.
on_startup in registration order, on_shutdown in reverse, so a thing
built on top of another is torn down first. Hooks may be sync or async. A
hook that raises is logged and the sequence continues: one application's
broken startup never prevents the others, and the ASGI acknowledgement is
always sent. Application errors are isolated; they never abort the protocol.
11. One thread pool, provisioned only if needed¶
Source: D2, SPECIFICATION.md:47. One monitored thread pool per server for blocking work; async handlers stay on the loop and never touch it; lazily provisioned, gauges exposed.
The gauges answer a question about pressure, so busy counts demand, not
slots held: every call entered and not yet returned, which past saturation
exceeds the slot count. Consumers clamp; the gauge does not flatter.
12. The installed composition, and self-configuration¶
Source: D22, SPECIFICATION.md:351. The core is the complete mono-process async server — that is the cut.
Source: D4, SPECIFICATION.md:67. The _server application is
automatic, not configured. Service endpoints are never again injected
into the hosted application's router — recorded against old finding F3.
Source: Ratified 2026-07-29, SPECIFICATION.md:772. Nothing materializes a server from the outside: the class that needs the values reads them. Explicitly passed kwargs win over configured ones, wholesale per kwarg. Detail in 015 configuration.
13. Restart is a target, not an afterthought¶
Source: D21, SPECIFICATION.md:280. Parity with the old daemon achieved WITHOUT a daemon: live pages survive ANY restart — state survives process generations, not inside an eternal process.
Source: D20, SPECIFICATION.md:273. Dev runs the shape of prod with minimal numbers; the internal server runs STANDALONE from the same configuration — no special inline mode, ever.
Both are owned by 120 restart. They appear here because they constrain what the base may hold that is not serializable — and because §4 reduces how often a restart is the answer at all.
14. Deliberately outside the base¶
Source: D7, SPECIFICATION.md:100. By decision, not by omission: the
config builder, middleware, real auth and sessions, _server, WSX. Each
arrives as its own capability, in its own entry of this world.
Open frictions¶
Evidence follow-up — 2026-09-08¶
- The universal startup-hook isolation wording conflicts with the explicit
FatalBootErrorstartup failure used by required SPA configuration. The unactionable-configuration refusal was owner-approved on August 23 and made an explicit startup requirement in approved profile design v0.3.1 on August 28 (historicaltemp/design_profili_fase2_2026-08-28.md, lines 81–82, 462 and T3/T4). The fatal exception is deliberate; precise reconciliation of the older universal hook wording remains open. - D7's empty socket is a phase-0 record superseded by the September WebSocket decisions. The base now owns a WebSocket registry as well.
- August uncovered-line counts are not current evidence; tests were split into core/SPA packages. Test inventory is not being reclassified here.
- Dynamic application installation and the D29 site index remain undelivered.
The earlier findings below retain their historical wording; the follow-up above and current status identify what still applies. No decision status is promoted by this audit.
Scaffolding for the interview, not a register. Each voice below is a question to settle; settling it edits this document — and, where the contradiction lives upstream, edits the source too. This section shrinks to nothing, and when it is empty this design can be ratified.
Interview file: temp/interview_010_server.md.
Upstream — settling these edits SPECIFICATION.md¶
Consequences of §4 to be decided¶
S5 [live-config] — what hot mount/unmount does to three properties. Today's immobility is load-bearing in three places, and §4 touches each:
- the demux reads the mount index with no lock, no snapshot and no invalidation, because nothing can change under it between two requests;
defaultis validated completely at construction, because no later registration can make a name valid that is invalid now;- the ownership channel is exactly-once, which is the natural meaning only while an application is never detached.
Are these properties to keep — so the hot command is built around them — or consequences of immobility that fall with it?
Partly answered on 2026-08-23: the second one survives, because accepting a
change is atomic and validated by the attempt, so default is never valid
against a tree nobody accepted. The first and the third are still open, and the
third has grown a second half — what "exactly once" means for an application
that is legitimately removed and put back.
S6 [live-config] — who watches, and at what granularity. The "refused when" half was settled on 2026-08-23: accepting is atomic and encloses the notification, so a failure changes nothing, and feasibility is not pre-computed — the attempt is the check. See 015 configuration §6.
What remains: whether the server watches on behalf of the applications or each application watches its own entry, and what becomes of one watching an entry that is deleted wholesale.
Defects and gaps in what exists¶
These are settled by fixing the code, not by amending a document; they are listed here so the interview can order them against the rest.
S7 [silent] — a multi-segment mount registers and is unreachable, silently. No
validation that a mount is a single path segment; the demux only ever matches
the first. Proven live in status.md: the application is installed, listed,
visible to the monitor, and dead — no error at boot, no error at request
time. mount="/api" fails the same way. The coding rules want an explicit
error for an impossible case; the probability rule accepts an infimum case
provided it ends noisily. This one is silent.
S8 [silent] — default= alongside a root application is accepted and inert.
Validated as a name, never consulted, because the root application always
wins first. Proven live in status.md. A configuration that says something and
gets nothing, without a word. Milder than S7, same family.
S9 [unread] — run_cleanups(error=...) has no caller and no reader. The parameter
is declared and documented as carrying the terminating exception for
error-aware cleanups; the body never reads it, the only production caller
passes nothing, no test passes it. Either the argument goes, or §8's mechanism
grows the error-aware half it promises.
S10 [untested] — two untested paths. AsgiServer.serve()'s host/port precedence
(the only uncovered statements of its module, and the path every deployment
takes) and the ValueError on an unsupported ASGI scope type. Details and
line numbers in status.md. The first is shared with
110 cli and 015 configuration.
S11 [unratified] — accepted risk: the pool teardown blocks the loop. The lifespan branch joins the worker threads synchronously on the event loop, after the shutdown acknowledgement has been sent — so the loop has nothing left to serve. Recorded as accepted pending confirmation; draining off the loop buys nothing at that point.
S12 [untested] — repo-wide: tests/x/ is empty. The coding rules mandate contract
tests and implementation/edge tests split by folder; the edge folder holds
only __init__.py, so every test is classified contract and every failure is
a STOP. Either correct as it stands, or a reclassification is owed — its own
task, never smuggled into another change. Not the server's to resolve;
recorded here because the server is the first subject to meet it.
Recorded in more than one entry¶
Found by a reader who had only the documents. Each lives between two or three entries, is written in the same words in each, and is settled once for all of them.