AshSandbox.EnvironmentTemplate (AshSandbox v0.1.2)

Copy Markdown View Source

A named deployment target within a project (003 T007).

defmodule MyApp.Environment do
  use AshSandbox.EnvironmentTemplate,
    data_layer: AshPostgres.DataLayer,
    domain: MyApp.Sandboxes,
    repo: MyApp.Repo,
    table: "environments",
    project_resource: MyApp.Project,
    registry_resource: MyApp.SandboxRegistry
end

preview, staging, production — backed by exactly one sandbox.

target_stack selects the mechanism

It lives here rather than on the sandbox because the mechanism is a property of what is being run, not of the running instance, and it must be known before a sandbox exists in order to create one (003 research R1).

Resolution is the host's (012-FR-001): ex_sandbox cannot assume the host is an OTP application named :axonn, so it supplies no configuration key. An unknown stack is an error naming the stack — never a fallback to a default, which would run tenant code under an isolation model nobody chose.

template_name is recorded even when the template is absent

So provisioning fails naming the missing template rather than starting an empty environment (003-FR-008, spec edge case). Validating its existence at creation time would be worse, not better: templates are built out of band and may be registered after an environment references one.

registry_resource is required, and it is what makes the allowlist writable

update accepts :network_allowlist and refuses while the environment has a live sandbox (029 T018 ruling) — see AshSandbox.Internal.RefuseAllowlistChangeWhileLive for why refusing is the honest shape. Deciding liveness means reading the host's registry, and the host names its own binding here rather than this library naming a module it cannot know (012-FR-009).

⚠️ Required rather than optional, on purpose. An optional reference has two shapes, and both are worse. Without it the guard is skipped — a control that reports success and changes nothing, the exact defect this exists to prevent — or the field silently stays unwritable and an operator meets "no such input" with nothing telling them why. A missing option is instead a compile error naming what to pass. (012-FR-015 makes this a breaking change to a public interface; that requirement is deferred until a version is published, and there is no published version.)

:unique_name_per_project compiles on any data layer

Its pre_check_with is derived from the host's data layer, the same way AshSandbox.RegistryTemplate's :unique_environment identity derives it — see AshSandbox.Internal.DataLayerSection.pre_check_with/2 for both branches and why neither is a safe default. Without this, Ash.DataLayer.Verifiers.RequirePreCheckWith refused to compile the resource on any data layer that cannot enforce a unique identity itself (ETS, Mnesia), which made this template AshPostgres-only in practice and contradicted 012-FR-009. ash_sandbox's own ETS HostApp fixtures now bind a Project and Environment, not only a registry.

purpose is here; the rule that reads it is not

An environment having a purpose — development, staging, production — is a general fact about environments, so it is an attribute of this template. What a purpose implies about availability is not general: it depends on the host's commercial arrangement with the owner, and a library that knows what :free and :paid mean has stopped being a library (derive-availability-from-plan-and-purpose design D2).

⚠️ purpose is never derived from name. The name is free text; a rule that read it would classify production-old and staging-2 wrongly, silently, and only for the hosts that happen to have named things that way.

availability_derivation: is how a host takes the choice away

Pass a change module and three things happen: :availability_mode leaves the accept lists of create and update, the module runs on both, and an update :rederive_availability appears that runs it and nothing else.

Omit it — as this library's own ETS HostApp fixtures do — and the template behaves exactly as it did: availability_mode stays caller input. The library keeps the field writable at the DSL level; it is the binding that removes the choice.

⚠️ The module must write availability_mode and idle_timeout_seconds together, because the validations below reject a mode/timeout pair that disagrees. It must also write them while the changeset is being built rather than in a before_action hook, since those validations run after the action's changes and before any hook.