Skip to content

domains

1ctl domains is the user-facing CLI surface for app hostnames. 1ctl domain is an alias for the same command group.

A domain workflow crosses several systems at once:

you
└─ 1ctl domains add api.example.com --app my-api
└─ Satusky API
├─ backend domain / ingress metadata
├─ Kubernetes HTTPRoute or Ingress
├─ DNS record state
└─ TLS certificate state

The CLI should hide most of that machinery during the happy path, but it must surface each layer when something is wrong. Domain management is only truly complete when the hostname recorded by the backend, the hostname routed by Kubernetes, the DNS answer on the public internet, and the certificate served over HTTPS all agree.

For the broader v1 product contract, see 1ctl v1 Contract.

Product boundary: domains own certificates

Section titled “Product boundary: domains own certificates”

Satusky should keep TLS under the domain workflow rather than introduce a public first-class 1ctl cert command today.

The current CLI already points in this direction: the old issuer command is hidden because cert-manager Issuer objects are implementation details, and custom-domain TLS is intended to be provisioned automatically when a domain is attached.

User concernRecommended CLI homeWhy
Add or remove a hostname1ctl domains ...The user is managing reachability for an app.
See whether HTTPS is ready1ctl domains check ...TLS readiness is part of whether a domain works.
Show required DNS/TLS setupplanned 1ctl domains setup ...DNS and certificate validation are one user journey.
Operate cert-manager Issuers directlynot a public v1 concernThat is backend implementation detail, not product vocabulary.

A separate public 1ctl cert command would only become justified later if Satusky intentionally supports user-operated certificate features such as:

  • importing custom certificates,
  • choosing between managed and user-supplied certificates,
  • manual certificate rotation,
  • certificate inventory independent from app domains.

Until then, splitting domains and certificates into separate top-level concepts would make the CLI more technical without making the user more capable.

AreaStatusNotes
List domainsImplemented1ctl domains list shows domain rows known to the current organization.
Add domainImplemented, but correctness gaps remain1ctl domains add <domain> --app <app> exists, but the current update path appears to discard explicit custom-domain changes on apps that already have an ingress. Tracked in satusky-core_backend#340.
Remove domainImplemented1ctl domains remove <domain> --app <app>.
Check domainPartially implementedCurrently metadata-only. It should become a real diagnostic command. Tracked in 1ctl#43 and satusky-core_backend#338.
Route reconciliationKnown gapBackend metadata and Kubernetes route state can drift. Tracked in satusky-core_backend#337.
Exact setup instructionsNot yet implementedNo domains setup flow yet. Tracked in 1ctl#48.
Aliases and www redirectsBackend exists, CLI missingBackend alias handlers exist, but 1ctl domains does not expose them yet. Tracked in 1ctl#49.
Canonical domain architectureKnown architecture gapLegacy ingress-domain and newer alias-domain models coexist. Tracked in satusky-core_backend#342 and 1ctl#50.
Partial-success handlingKnown correctness gapSome backend mutations can return success after route/TLS reconciliation fails. Tracked in satusky-core_backend#341.
Routing primitive consistencyMixed architecture todayDefault/generated domains use Gateway API HTTPRoute, but custom domains still use legacy Kubernetes Ingress. Tracked in satusky-core_backend#343 and satusky-core_backend#344.
Terminal window
1ctl domains list
1ctl domains add <domain> --app <app> [--port <port>] [--custom-dns]
1ctl domains remove <domain> --app <app> [-y]
1ctl domains check <domain>
Terminal window
1ctl domains list

Lists domains known to the current organization.

Example

DOMAIN APP TLS CREATED
happygiraffe-3u7lttk.satusky.com diagnosis-bot platform 5 minutes ago
Terminal window
1ctl domains add <domain> --app <app> [--port <port>] [--custom-dns]

Attaches a domain to an app through the current CLI path.

FlagDefaultDescription
--apprequired todayApp name, matching [app].name in satusky.toml or --name on deploy.
--port8080Target service port.
--custom-dnsfalseUse Let’s Encrypt TLS for non-*.satusky.com hostnames.

Examples

Terminal window
1ctl domains add api.example.com --app my-api --custom-dns
1ctl domains add internal.example.com --app internal-api --port 3000 --custom-dns

Current limitation. The installed CLI currently calls the generic ingress upsert path. In backend controllers/ingress_controller.go, UpsertIngress preserves the existing hostname on update by assigning metadata.DomainName = existingIngress.DomainName. That means an explicit custom-domain request on an app that already has a generated hostname appears likely to be ignored instead of applied. This is tracked in satusky-core_backend#340.

Terminal window
1ctl domains remove <domain> --app <app> [-y]

Removes a domain from an app.

FlagDefaultDescription
--apprequired todayApp name, matching [app].name in satusky.toml.
--yes, -yfalseSkip the confirmation prompt.

Example

Terminal window
1ctl domains remove api.example.com --app my-api -y
Terminal window
1ctl domains check <domain>

Checks the platform’s current recorded view of a domain.

Current output

Domain api.example.com
App: my-api
Namespace: orgv1-80c8588b
TLS: Platform-managed (*.satusky.com)
Created: 5 minutes ago

Current limitation. Despite the command name, the current implementation in 1ctl/internal/commands/domains.go only renders ingress metadata. It does not yet show route attachment, DNS propagation, certificate status, or public reachability.

The backend is not yet Gateway API-only. The code currently uses two Kubernetes routing primitives:

Hostname typeCurrent Kubernetes resourceEvidence in backend code
Default / generated *.satusky.com domainsGateway API HTTPRoute when INGRESS_MODE=gateway-apigatewayRoutingConfigForDefaultDNS, buildHTTPRouteSpec, and services/network/httproute.go
Custom domains, aliases, and www redirectslegacy Kubernetes IngressDnsConfigCustom always falls through to the Ingress + cert-manager path, upsertCustomK8sIngress, buildCustomIngressSpec, and handleWWWRedirect
current routing split
default platform domains ──► HTTPRoute
custom domains ──► legacy Ingress

That mixed architecture is real and should be documented plainly. If Gateway API is the chosen platform standard, the legacy Ingress path should be treated as a compatibility bridge, not the desired long-term model.

Planned target, not implemented yet:

future routing model
default generated hostname ──► HTTPRoute
custom domain aliases ──► HTTPRoute
TLS ──► Gateway-compatible certificate flow
Kubernetes Ingress ──► deprecated compatibility path, then removed

The migration work is tracked in satusky-core_backend#343. Formal deprecation/removal of the legacy Ingress path is tracked in satusky-core_backend#344.

A durable domain system needs one coherent source-of-truth model across four planes:

PlaneResponsibilityExample question
Backend metadataOwnership and desired attachmentWhich app owns api.example.com?
Routing stateKubernetes HTTPRoute or IngressIs that hostname actually routed to the app?
DNS statePublic record resolutionDoes the hostname resolve to Satusky’s edge?
TLS statecert-manager or platform-managed TLSCan HTTPS be served correctly for that hostname?
backend desired state
route reconciliation ──► DNS expectation ──► TLS issuance
│ │ │
└──────── domains check should report all four ────────┘

The user should not have to know which subsystem owns each plane on a good day. But when one layer disagrees with another, the CLI should expose the disagreement instead of collapsing everything into a misleading success badge.

1. Explicit custom-domain updates can be discarded

Section titled “1. Explicit custom-domain updates can be discarded”

The normal post-deploy workflow is supposed to be:

Terminal window
1ctl domains add api.example.com --app my-api

But the current backend update path appears to overwrite the requested hostname with the existing hostname whenever the app already has an ingress. That makes the add/update workflow unreliable for the common case of attaching a custom domain after the app already has its generated default domain.

Tracked in satusky-core_backend#340.

A verified failure showed this mismatch:

PlaneHostname
Backend domain recordhappygiraffe-3u7lttk.satusky.com
Kubernetes HTTPRoutesilentgiraffe-6z2enyu.satusky.com

The old hostname remained live and routed traffic to the new pod, while the new hostname did not resolve. The platform must treat the database record and the actual Kubernetes route as a reconciled unit, not two facts that can silently diverge.

Tracked in satusky-core_backend#337.

3. Mutation success does not yet prove reconciliation success

Section titled “3. Mutation success does not yet prove reconciliation success”

The newer alias/custom-domain backend path already contains useful functionality, but several handlers can insert or remove database rows, then log a warning if the Kubernetes Ingress or cert-manager Issuer update fails, and still return ordinary success to the caller.

A domain mutation should either:

  1. finish all required write-side reconciliation before returning success, or
  2. return an explicit operation state such as pending, reconciling, or failed that users can inspect later.

It should not silently tell the user everything worked when only the database write worked.

Tracked in satusky-core_backend#341.

The backend currently contains both:

  • a legacy primary-hostname ingress model, and
  • a richer newer model with owned domains, aliases, DNS records, and optional www redirects.

The richer model appears closer to the right long-term product shape, but the current CLI mostly drives the older ingress-oriented path. That split is expensive because it lets different callers choose different semantics for the same user concept.

Recommended direction:

default generated hostname = platform route
custom domains = aliases attached to that app route
DNS / TLS / redirects = reconciled from the canonical domain model

Tracked in satusky-core_backend#342 and 1ctl#50.

5. Custom domains still depend on legacy Kubernetes Ingress

Section titled “5. Custom domains still depend on legacy Kubernetes Ingress”

Even with INGRESS_MODE=gateway-api, only DnsConfigDefault routes move to Gateway API today. Custom domains explicitly fall back to the older Ingress + cert-manager path, and alias / www redirect helpers still create networkingv1.Ingress objects.

This means custom-domain work currently carries both architectural migrations at once:

  1. unify the domain data model, and
  2. migrate the routing primitive from Ingress to HTTPRoute.

Recommended direction:

custom domains should become Gateway API-native
legacy Kubernetes Ingress should be deprecated after migration

Tracked in satusky-core_backend#343 and satusky-core_backend#344.

domains check should become a real diagnostic command

Section titled “domains check should become a real diagnostic command”

Planned, not implemented yet. A complete check should report each plane separately:

CheckWhy it matters
Backend ownershipConfirms the domain is registered to an app and namespace.
Route attachmentConfirms an HTTPRoute or Ingress exists for the same hostname.
DNS recordsConfirms the hostname resolves to the expected edge target.
TLS stateConfirms cert-manager or platform-managed wildcard TLS is ready.
Public probeConfirms https://<domain>/ actually reaches the app.

Target output shape

Domain: api.example.com
Backend: attached to my-api in orgv1-80c8588b
Route: attached to HTTPRoute/my-api-route
DNS: resolves to 172.66.40.108, 172.66.43.148
TLS: ready
HTTP: reachable

Tracked in 1ctl#43 and satusky-core_backend#338.

domains setup should print exact next steps

Section titled “domains setup should print exact next steps”

Planned, not implemented yet. After attaching a custom domain, users should not be told only to “point an A record at the platform LoadBalancer IP.” The CLI should print the actual required DNS records and current validation state.

Target command

Terminal window
1ctl domains setup api.example.com

Target output shape

Domain: api.example.com
App: my-api
Required DNS records:
A api.example.com 203.0.113.10
DNS: propagating
TLS: provisioning
Next check: 1ctl domains check api.example.com

Tracked in 1ctl#48.

Alias and www redirect workflows should be exposed

Section titled “Alias and www redirect workflows should be exposed”

Planned, not implemented yet. Backend alias support already exists, but the current CLI cannot show one app serving multiple hostnames or expose the optional www redirect workflow.

Target shape

Terminal window
1ctl domains aliases list --app my-api
1ctl domains aliases add api.example.com --app my-api
1ctl domains aliases remove api.example.com --app my-api
1ctl domains add example.com --app my-api --with-www-redirect

Tracked in 1ctl#49.

  1. Fix correctness before expanding UX

    • explicit custom-domain updates must not be discarded (backend#340)
    • route/domain drift must be reconciled (backend#337)
    • partial-success responses must become truthful (backend#341)
  2. Choose the canonical model

    • unify the legacy ingress path and newer alias path (backend#342)
    • migrate 1ctl domains to that canonical API (1ctl#50)
  3. Finish the Gateway API migration

    • move custom domains from legacy Ingress to HTTPRoute (backend#343)
    • formally deprecate and remove the old Ingress path after migration (backend#344)
  4. Make diagnostics first-class

    • enrich the backend status API (backend#338)
    • make domains check genuinely diagnostic (1ctl#43)
    • add exact setup guidance (1ctl#48)
  5. Expose richer product behavior

    • alias and www redirect workflows (1ctl#49)
    • rewrite the older custom-domain guide around the current product model (satu-docs#2)

Until reconciliation and diagnostics are fully implemented, verify all four planes when debugging a suspicious domain:

Terminal window
1ctl domains list
1ctl domains check <domain>
kubectl -n <namespace> get httproute,ingress -o wide
dig +short <domain> A
curl -k https://<domain>/health

That manual sequence is exactly what the finished domains check command should eventually collapse into one truthful answer.