We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Jump to file
- ∟ Caddyfile
- ∟ README.md
- ∟ config/dev.exs
- ∟ config/runtime.exs
- ∟ config/test.exs
- ∟ lib/my_app/hosting.ex
- ∟ lib/my_app/hosting/certificates.ex
- ∟ lib/my_app/hosting/certificates/fly_adapter.ex
- ∟ lib/my_app/hosting/certificates/local_adapter.ex
- ∟ lib/my_app/hosting/certificates/mock_adapter.ex
- ∟ lib/my_app/hosting/dns.ex
- ∟ lib/my_app/hosting/dns/live_resolver.ex
- ∟ lib/my_app/hosting/dns/local_resolver.ex
- ∟ lib/my_app/hosting/hostname.ex
- ∟ lib/my_app/hosting/site.ex
- ∟ lib/my_app_web/controllers/hosted_controller.ex
- ∟ lib/my_app_web/endpoint.ex
- ∟ lib/my_app_web/live/hostname_live/form.ex
- ∟ lib/my_app_web/live/hostname_live/show.ex
- ∟ lib/my_app_web/live/site_live/form.ex
- ∟ lib/my_app_web/live/site_live/index.ex
- ∟ lib/my_app_web/live/site_live/show.ex
- ∟ lib/my_app_web/plugs/serve_hosted_site.ex
- ∟ lib/my_app_web/router.ex
- ∟ lib/my_app_web/routers/hosted_router.ex
- ∟ priv/repo/migrations/0001_create_sites.exs
- ∟ priv/repo/migrations/0002_create_hostnames.exs
- ∟ test/my_app/hosting/certificates_test.exs
- ∟ test/my_app/hosting/dns_test.exs
- ∟ test/my_app/hosting_test.exs
- ∟ test/my_app_web/live/hostname_live_test.exs
- ∟ test/my_app_web/live/site_live_test.exs
- ∟ test/support/fixtures/hosting_fixtures.ex
Caddyfile
The Caddyfile configures Caddy as a local reverse proxy for testing. It routes three types of requests to the Phoenix server: the main app domain (
my-app.test
), wildcard subdomains (
*.my-app.test
), and a test custom domain (
my-hostname.test
). All use plain HTTP since Caddy handles TLS termination in production but not needed locally.
|
|
|
|
|
|
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
|
|
|
Dev configuration
Development uses
my-app.test
as the endpoint host (instead of
localhost
) so the
ServeHostedSite
plug can distinguish between the main app and subdomain/hostname requests. The DNS resolver and certificate adapter are set to their local variants that auto-verify and auto-succeed.
|
|
|
|
|
|
|
|
| - |
|
| + |
|
| + |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
|
Production configuration
Production configures the Fly.io certificate adapter with the app ID and API token, the live DNS resolver, and a CNAME target derived from the Fly app name.
check_origin: :conn
is set on the endpoint to allow WebSocket connections from any origin, since hosted sites will connect from their custom domains.
|
|
|
|
|
|
|
|
| + |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| + |
|
|
|
|
|
|
Hosting context
The
Hosting
context manages sites and hostnames with scoped access (users can only manage their own resources) and PubSub broadcasting for real-time updates.
Sites
— Standard CRUD operations scoped to the current user.
get_site_by_subdomain/1
and
get_site_by_hostname/1
are unscoped lookups used by the routing plug to resolve incoming requests to sites.
Hostnames — CRUD operations scoped to a site. Creating a hostname auto-generates the TXT verification secret. Deleting a hostname also cleans up any associated certificate with the certificate provider.
DNS verification
—
verify_hostname/2
delegates to the DNS resolver to check for the expected TXT record. On success, it timestamps
verified_at
on the hostname.
verification_instructions/1
and
cname_instructions/1
return the DNS records the user needs to configure.
Certificate management
—
issue_certificate/2
requires a verified domain and delegates to the certificate adapter. It tracks both
certificate_requested_at
(immediately) and
certificate_issued_at
(when the adapter confirms issuance, which may be async).
refresh_certificate_status/2
polls the adapter for pending certificates.
delete_certificate/2
removes the certificate and clears the timestamp fields.
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
Certificates module
The
Certificates
module provides a behaviour-based interface for TLS certificate management. It defines three callbacks:
add_certificate/2
,
check_certificate/2
, and
delete_certificate/2
, each taking a hostname string and adapter-specific options.
The public API functions (
issue_certificate/1
,
check_certificate/1
,
delete_certificate/1
) accept a
Hostname
struct, extract the domain, and delegate to the configured adapter with its options. This keeps the hosting context decoupled from any specific certificate provider.
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
Fly.io certificate adapter
The production adapter uses Fly.io's GraphQL API to manage TLS certificates. Fly automatically provisions Let's Encrypt certificates for custom domains added to an app.
add_certificate/2
calls the
addCertificate
mutation to register a domain.
check_certificate/2
queries the certificate status including issued certificate details (type, expiration).
delete_certificate/2
removes the certificate.
The adapter parses Fly's response into a normalized
certificate_result
map with status (
:pending
or
:issued
), validation details, and expiration. Certificate status is determined by whether any issued certificate nodes exist in the response.
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
Local certificate adapter
The development adapter logs certificate operations and immediately returns success. Since local development uses Caddy (which handles its own TLS or runs over HTTP), there's no real certificate to provision. All operations return
:issued
status instantly so the full flow can be tested locally.
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
DNS verification module
The DNS module handles domain ownership verification through TXT records. Users prove they own a domain by adding a TXT record at
_myapp-verify.<domain>
containing
myapp-verification=<secret>
.
The module defines a
@callback lookup_txt/1
behaviour so the resolver is swappable between environments.
verify_domain/1
looks up TXT records at the verification hostname and checks if any match the expected value.
Helper functions
verification_host/0
,
verification_hostname/1
, and
verification_record_value/1
build the DNS record details shown to users.
cname_target/0
returns the hostname users should CNAME their domain to (configurable, defaults to
proxy.fly.dev
).
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
Live DNS resolver
The production resolver uses Erlang's
:inet_res
module for real DNS lookups. It handles the quirks of TXT records: they come back as lists of charlists (since TXT records can be split into 255-byte chunks) that need to be joined and converted to strings. An empty result from
:inet_res.lookup/4
is ambiguous (could be no records or NXDOMAIN), so it falls back to
:inet_res.resolve/4
to distinguish between the two.
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
Local DNS resolver
The development resolver auto-verifies all domains by looking up the hostname's
txt_record_secret
directly from the database and returning it as a TXT record. This means clicking "Verify" in development always succeeds without needing actual DNS configuration. It parses the
_myapp-verify.
prefix from the lookup hostname to find the actual domain, then queries the
Hostname
record.
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
Hostname schema
The
Hostname
schema stores custom domains pointed at a site. Each hostname has a
domain
, a
txt_record_secret
for DNS ownership verification, and timestamp fields tracking the verification and certificate lifecycle:
verified_at
,
certificate_requested_at
, and
certificate_issued_at
.
When a hostname is first created,
maybe_generate_secret/1
generates a cryptographically random 32-byte token (base64-encoded) that becomes the TXT record value the user must set. Domain validation uses a regex that enforces valid DNS domain format and a unique constraint ensures no two hostnames share the same domain globally.
Separate changesets handle different stages of the lifecycle:
verification_changeset/2
sets the verified timestamp, and
certificate_changeset/2
updates certificate fields.
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
Site schema
The
Site
schema represents a hosted site belonging to a user. Each site has a
name
and a unique
subdomain
used for subdomain-based routing (e.g.,
my-site.my-app.test
).
The
changeset/4
function validates that subdomains are lowercase alphanumeric with hyphens, enforces a max length of 63 characters, and ensures uniqueness. A
cast_default_subdomain
option auto-generates a subdomain from the site name by lowercasing and replacing non-alphanumeric characters with hyphens, providing a nice UX when creating sites.
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
Endpoint configuration
The
ServeHostedSite
plug is added to the endpoint just before
MyAppWeb.Router
. This placement is important: it needs access to the session (for CSRF in the hosted router) but must intercept requests before the main router handles them.
|
|
|
|
|
|
|
|
| + |
|
| + |
|
|
|
|
Hostname LiveView - Form
A simple form for adding custom domains to a site. After creating a hostname, it redirects to the hostname show page where the user can see the DNS verification instructions.
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
Hostname LiveView - Show
The hostname detail page shows the domain, current status (Pending verification / Verified / Certificate pending / Active), and DNS configuration instructions. The page guides users through the setup process:
Once the certificate is issued, the DNS configuration section hides and the status shows "Active".
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
Sites LiveView - Form
Handles both creating and editing sites with name and subdomain fields. Uses the standard Phoenix LiveView form pattern with validation on change and save on submit.
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
Sites LiveView - Index
A standard list page showing all sites for the current user in a table with name and subdomain columns. Supports creating, editing, and deleting sites with real-time updates via PubSub subscriptions.
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
Sites LiveView - Show
The site detail page displays the site info and a table of associated hostnames with their verification status. Users can add new hostnames, view hostname details, and delete hostnames. Subscribes to both site and hostname PubSub topics for real-time updates.
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
ServeHostedSite plug
This plug sits in the endpoint pipeline before the main router and intercepts requests destined for hosted sites. It compares the request host against the endpoint's configured host to determine routing:
When a site is found, the plug assigns
current_site
to the conn and dispatches to the
HostedRouter
. If no site matches, it returns a 404. The plug calls
halt()
after dispatching to the hosted router, preventing the request from continuing to the main app router.
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
Router
Site and hostname routes are added under the authenticated scope. Sites get full CRUD routes, while hostnames are nested under sites with routes for creating and viewing.
|
|
|
|
|
|
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
|
|
|
Hosted router and controller
The
HostedRouter
is a separate router for hosted site requests. It has its own browser pipeline (session, CSRF, etc.) and a catch-all route that sends all paths to
HostedController.show/2
. The controller simply renders a success page with the site name — in a real app, you'd replace this with your hosted site rendering logic.
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
Migrations
Two migrations create the
sites
and
hostnames
tables. Sites have a unique index on
subdomain
and a foreign key to
users
. Hostnames have a unique index on
domain
(globally unique across all sites) and a foreign key to
sites
with cascade delete.
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
Install with Claude Code
Write instructions to implement this feature to your project directory in a LLM-friendly format, then have Claude take care of the rest! Requires Claude Code to be installed.
curl "https://elixir-saas.com/llms/p/dns.md?v=1.8.3&f=impl" > dns.md;curl "https://elixir-saas.com/llms/p/dns.md?v=1.8.3&f=test" > dns_tests.md;claude "Implement the feature that is documented in: dns.md, then add the tests documented in: dns_tests.md." --allowedTools "Write Edit Bash(mix:*) Bash(mkdir:*)";