Skip to content

docs/DX: TypeScript AppHost model — restart semantics, hand-edit-vs-regen, service→plugin references, native-driver permissions #264

Description

@rickylabs

docs/DX: the TypeScript AppHost model — hand-edit-vs-regen, service→plugin references, native-driver permissions

Filed as dogfooding feedback from wiring eis-chat (an AI-stack app) to @netscript/* 0.0.1-alpha.19 and standing up MCP-over-HTTP services. The framework itself worked; the friction below was almost entirely discoverability — the runtime model is great, but a few load-bearing facts about the generated Aspire AppHost are only learnable by reading generated code or hitting a boot crash. One of the five (service→plugin refs) reads like a genuine framework gap, not just docs.

Everything here is grounded in real incidents. Where an existing doc already covers something, I say so and scope the ask down.

Context for the reader: the generated model in this app is aspire/apphost.mts.helpers/register-{infrastructure,services,plugins,background,apps,tools}.mts, driven by root appsettings.json (NetScript.{Services,Plugins,BackgroundProcessors,Apps}). Each register-*.mts is headed // GENERATED BY @netscript/cli. DO NOT EDIT. Re-run 'netscript generate' ....


1. Restart semantics: which changes need a full aspire run relaunch vs a per-resource restart

What happened. Adding new resources / new references in appsettings.json + register-*.mts did not take effect on a per-resource restart from the dashboard — the AppHost has to be re-materialized (netscript generate if config changed, then a full aspire run relaunch). We initially assumed restarting the changed resource would pick it up; it silently didn't, because the resource graph is fixed at builder.build().run() time.

What's already covered (credit where due).

  • how-to/discover-services.md does state the ServiceReferences path explicitly: "Adding a ServiceReferences entry does nothing until you re-run netscript service generate and restart aspire start." Good — for that one field.
  • Aspire's own Hot Reload and watch doc covers the general principle (AppHost-model changes restart the whole app; per-resource restart doesn't re-evaluate the model; defaultWatchEnabled is opt-in and restart-based).

The gap. Neither generalizes the rule to all AppHost-model changes in a NetScript project. There's no single "which change needs which restart" table for the NetScript layer, e.g.:

Change Per-resource restart enough? Needs netscript generate? Needs full relaunch?
Service/plugin code edit (with watch/HMR) yes no no
New service/plugin/app/background-processor in appsettings.json no yes yes
New ServiceReferences / PluginReferences no yes yes
Permission-flag change no yes yes
Env-var value change in appsettings.json no (regen) yes

Proposal. A short "Restart & regeneration semantics" section on explanation/aspire.md (or the Aspire reference) with a table like the above — generalizing the rule that discover-services.md already states for ServiceReferences to the whole config surface.


2. Hand-edit-vs-regen: the "DO NOT EDIT" header has no documented escape hatch

What happened. explanation/aspire.md is emphatic and correct for the common case: "You do not edit apphost.mts to add a service… you never enumerate processes by hand." But real wiring in this app had to be hand-edited into the generated register-*.mts files, and there is no documented story for it:

Each is tagged in-repo with a HAND-EDITED (keep on regen) comment — a convention we invented precisely because the docs don't offer one. The tension: the header says don't edit, the framework's own netscript generate will silently drop anything you add, and yet there's no config-only way to express these cases. So users are stuck between "can't do it in config" and "editing a file that says don't."

The gap. No documented answer to: what's safe to hand-edit, how do I survive netscript generate, and which of these should instead be expressible in appsettings.json? (Note: this app already leans on the appsettings.json Services.<name>.Env/Permissions fields for the MCP case, so some of it is config-expressible — that pattern just isn't documented as the sanctioned path.)

Proposal. A "Customizing the generated AppHost" how-to that covers: (a) the set of things fully expressible in appsettings.json today (custom Env, Permissions, Port, Workdir per resource — these already work and are under-advertised); (b) for anything not config-expressible, whether netscript generate has (or should have) a merge/preserve mechanism (e.g. a # @netscript:keep marker, or user-owned register-*.custom.mts hooks that survive regen); (c) an honest "here be dragons, these edits are dropped on regen" note if no preserve mechanism exists yet.


3. Native DB drivers force Deno permissions that the generator doesn't grant to every resource that needs them

What happened (concrete boot crash). alpha.19 @netscript/plugin-workers loads @tursodatabase/database, a native FFI addon. The generated launch args for the workers background processor (and initially workers-api) lacked --allow-ffi, so the process crashed at boot with NotCapable: Requires ffi access. Sibling resources (streams, and eischat after its own fix) already had --allow-ffi, so it was inconsistent across resources that all ultimately touch the same native driver. Fixed in eis-chat by hand-adding --allow-ffi (commit 6fa91d7).

Interesting related detail: netscript #174 reportedly added --allow-ffi for DB-driver services in .19 — but not for plugin-workers' processor/API executables, which is exactly the one that broke here.

What's already covered. how-to/restrict-worker-task-permissions.md documents the Deno task permission path well (.permissions({ ffi: true })--allow-ffi, the buildDenoPermissionFlags table, the --allow-all footgun). That's the worker-task-adapter surface.

The gap. That's a different layer from resource-level launch permissions — the resolvePermissions(...) flags baked into each builder.addExecutable(...) in register-{services,plugins,background}.mts. There's no doc on: which resource archetype gets which default permission set, and — the sharp edge — that a plugin whose runtime pulls a native DB driver (@tursodatabase/database, libsql) requires --allow-ffi/--allow-sys on every executable that loads it (API and background processor), not just the "database service." A one-liner mapping native-driver → required flags, and a note that plugin-workers' executables need --allow-ffi in .19, would have turned a boot crash into a config line.

Proposal. Extend the permissions how-to (or the plugin-workers reference) with a "resource-level launch permissions vs task permissions" distinction + a native-driver → required-flags note. If plugin-workers should self-declare --allow-ffi on its own executables via its Aspire contribution, that's arguably a framework fix too (partner to #174).


4. A Service cannot declare PluginReferences — likely a framework gap, not just docs

What happened. eis-chat's eischat service needs to enqueue KB-ingestion jobs on workers-api (a plugin). Its workersClient (serviceName workers-api) resolves the URL via the standard service-discovery env var services__workers-api__http__0 — exactly the mechanism how-to/discover-services.md documents. But there was no way to declare that dependency in config:

  • Apps and BackgroundProcessors accept ServiceReferences and PluginReferences in appsettings.json (this app uses Apps.dashboard.{ServiceReferences,PluginReferences} and BackgroundProcessors.workers.PluginReferences: ["workers-api"], both auto-wired).
  • A Service entry has no PluginReferences field, so a service→plugin edge is unrepresentable. We wired it by hand in register-background.mts — mapping workers-api's endpoint onto the eischat service resource manually (commit aa0da26).

Why this reads as a framework gap. explanation/aspire.md presents ServiceReferences/PluginReferences as the generic reference vocabulary ("@netscript/aspire extracts three kinds of dependency from each entry") and even gives workers-api → sagas-api as a PluginReferences example — but the worked examples only ever put these on Apps/BackgroundProcessors, and the ServiceEntry schema apparently doesn't carry PluginReferences. So a service calling a plugin API — a totally ordinary edge (any service enqueuing background work through a plugin's API hits it) — falls through. The two-pass resolver already knows how to inject services__<plugin>__http__0; it's "just" that ServiceEntry doesn't expose the field to opt in.

Proposal (framework, with docs to match). Let ServiceEntry declare PluginReferences (and, symmetrically, ServiceReferences if not already) so register-services.mts wires the service→plugin endpoint in pass 2 the same way it does for Apps/BackgroundProcessors — removing the need for the manual cross-map. If there's an intentional reason services can't reference plugins, documenting that (and the sanctioned workaround) would resolve it on the docs side. Either way explanation/aspire.md's reference-fields table should state, per resource archetype, which reference kinds are actually accepted.


5. Service-discovery convention & two-pass wiring — already well documented (dropping this)

Listed for completeness only. When we first hit the services__<name>__http__0 / VITE_services__… convention and the "create all, then wire refs via getEndpoint('http')" two-pass model, it felt undocumented — but it isn't:

  • how-to/discover-services.md documents the env-var convention, the two-pass registration, the VITE_-prefixed browser form, and even the manual/non-Aspire resolution (getServiceUrl, getAllServices).
  • explanation/aspire.md has the full two-pass diagram and the reference-field semantics.

So no ask here — this friction was self-inflicted (we read generated code before finding the how-to). Flagging it so this issue doesn't over-claim. The one adjacent improvement is that these two good pages could be cross-linked from the register-*.mts header comment or a netscript generate output line, so the next person lands on them before reading generated code.


Summary of asks

# Type Ask
1 docs Generalize the "regenerate + full relaunch" rule (already stated for ServiceReferences) into a restart-semantics table for the whole config surface.
2 docs (+ maybe framework) "Customizing the generated AppHost" how-to: what's config-expressible today, and whether netscript generate preserves hand-edits.
3 docs (+ maybe framework) Resource-level launch permissions vs task permissions; native-driver → required-flags note; plugin-workers executables need --allow-ffi in .19 (partner to #174).
4 framework (+ docs) Let a Service declare PluginReferences (service→plugin edge), or document why not + the workaround.
5 No ask; already covered by discover-services.md. Optional: cross-link it from generated-file headers.

Links / references

Non-prescriptive throughout — happy to be wrong about #4 being a framework gap if services-can't-reference-plugins is intentional; in that case this is purely a docs ask. Thanks for a genuinely good runtime model; this is all about making the AppHost layer as legible as the rest of it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions