Skip to content

Cloudflare DNS Pointing Design ​

Goals ​

Point ZoneDomains (explicit FQDNs) in OpenFlare to edge node IPs quickly via the Cloudflare API, replacing manual A-record edits in the CF console. Users organize domains into pointing groups: each group configures a primary node, a backup node, and a default orange-cloud (proxied) policy; members can override orange-cloud individually. The system treats the database tables as the desired state and idempotently syncs remote DNS.

This module is an optional integration capability; it does not turn Zones into an authoritative DNS control plane. Zones still only handle root-domain boundaries, domains, certificates, and reverse-proxy associations; A record create/update/delete is driven by this module through Cloudflare.

Scope and Phasing ​

Phase 1 (this design's scope) ​

  • Sidebar Cloudflare entry with a Token-ready gate
  • Connection config: import from an existing DNS account or standalone entry within the module (mixed sources), stored encrypted
  • Pointing group CRUD: primary node, backup node (reserved), group default orange-cloud
  • Member management: add/remove at the granularity of zone_domain_id; member-level orange-cloud
  • Sync: write each member as a single A record on Cloudflare → current active node IPv4
  • Triggers: manual sync, member add, node/orange-cloud change, node IP change enqueue
  • Async task batch sync; member sync status with readable errors

Phase 2 ​

  • Agent heartbeat offline detection of primary node failure → active_node switches to backup → whole-group auto sync
  • Optional auto failback, failure notification push

Explicitly Out of Scope (later or permanent) ​

  • Multiple parallel Cloudflare accounts (one global connection config)
  • AAAA / multi-A load balancing / CNAME to node hostnames
  • Managing MX/TXT/Page Rules and other non-module A records
  • Non-Cloudflare DNS providers
  • Merging DNS record management into the Zone core model

Relationship with Existing Capabilities ​

Existing CapabilityRelationship
of_zones / of_zone_domainsProvide the pointable FQDN list; this module only references zone_domain_id
of_nodes.ipSource of A record content; recommended to restrict to edge nodes with valid IPv4
of_dns_accounts + sealSensitiveACME DNS-01 already supports Cloudflare Token; this module can import the same account or store a Token standalone
lego Cloudflare providerOnly TXT/DNS-01; this module builds its own CF HTTP client for Zone/DNS Record APIs

Core Model ​

mermaid
erDiagram
  CF_CONNECTIONS ||--o| DNS_ACCOUNTS : optional_import
  CF_POINTING_GROUPS ||--o{ CF_POINTING_MEMBERS : contains
  ZONE_DOMAINS ||--o| CF_POINTING_MEMBERS : pointed_as
  NODES ||--o{ CF_POINTING_GROUPS : primary
  NODES ||--o{ CF_POINTING_GROUPS : backup
  NODES ||--o{ CF_POINTING_GROUPS : active

  CF_CONNECTIONS {
    uint id PK
    string source
    uint dns_account_id
    string authorization
    string status
    time verified_at
  }
  CF_POINTING_GROUPS {
    uint id PK
    string name
    uint primary_node_id
    uint backup_node_id
    uint active_node_id
    bool default_proxied
    bool enabled
  }
  CF_POINTING_MEMBERS {
    uint id PK
    uint group_id
    uint zone_domain_id UK
    bool proxied
    string cf_zone_id
    string cf_record_id
    string desired_ip
    string sync_status
    string last_error
    time synced_at
  }

of_cf_connections (one valid connection globally) ​

FieldDescription
sourcedns_account | standalone
dns_account_idreferences of_dns_accounts (type=cloudflare) when source=dns_account
authorizationencrypted storage when source=standalone, payload shape {"api_token":"..."}, consistent with DNS accounts; the API never returns it
status / verified_atconnectivity check result and time

Token resolution: dns_account → decrypt the associated account; standalone → decrypt this row. Associated account deleted or validation failed → module not ready, sync forbidden.

Recommended permissions: Cloudflare API Token with Zone:Read, DNS:Edit.

of_cf_pointing_groups ​

FieldDescription
namedisplay name
primary_node_idprimary node
backup_node_idbackup (nullable; phase 1 stores only)
active_node_idcurrently effective node; equals primary in phase 1; rewritten by phase 2 failover
default_proxiedgroup default orange-cloud; only affects newly added members
enabledwhether to participate in sync

Constraints: primary and backup must not be the same node; the node chosen as the active target must have a valid IPv4.

of_cf_pointing_members ​

FieldDescription
group_idowning group
zone_domain_idglobally unique: one domain belongs to at most one group
proxiedmember orange-cloud (the only runtime basis)
cf_zone_id / cf_record_idCloudflare cache for idempotent updates
desired_ip / sync_status / last_error / synced_atdesired and sync state

sync_status: pending | syncing | ok | error.

No physical foreign keys; zone_domain_id unique index; query indexes on group_id etc.

Orange-Cloud Priority ​

  1. Member proxied: the only basis written to CF during sync.
  2. Group default_proxied: copied to proxied when a member is added.
  3. Later changes to the group default do not rewrite existing members.

Sync Semantics ​

Desired State ​

The OpenFlare DB tables are the Source of Truth. Each member expects:

ItemValue
typeA
namethe ZoneDomain's FQDN
contentthe group active_node's IPv4
proxiedmember proxied
ttlforced Auto by CF when orange-cloud is on; unified default (e.g. 300) when off

Phase 1 does not write AAAA. Node IP not a valid IPv4 → that member is error.

Triggers ​

TriggerBehavior
Manual sync (all / group / member)reconcile
Member addedinitialize proxied, then enqueue sync
Member removed / group deleteddelete the remote A managed by this module by default (configurable keep)
Primary node / active / member proxied changedre-sync the corresponding scope
Node IP changed (heartbeat or manual)enqueue members whose active_node_id points to that node
Token not readyrefuse sync

Phase 1 does not do scheduled full reconciliation.

Reconcile (single member, idempotent) ​

  1. Resolve the CF Zone by the FQDN's registrable root domain, cache cf_zone_id.
  2. With a cf_record_id, prefer Update; if stale, list by name+type=A.
  3. 0 records → Create; exactly 1 → take over and Update; multiple → fail and tell the user to clean up in CF.
  4. Write back cf_record_id, desired_ip, sync_status, synced_at / last_error.
  5. On rate limiting, retry with bounded backoff.

Ownership: only manage records cached by this module or taken over as "the only same-name A"; do not clear the Zone or touch other record types. After a user edits in the CF console, the next sync overwrites with the OpenFlare desired state.

Execution Carrier ​

  • Single record: can sync on the request path.
  • Whole group / per-node batch: Asynq tasks (cloudflare:sync_member / sync_group / sync_by_node), registered in bootstrap.
  • Per-member mutex to prevent concurrent double-writes.
  • Node IP change path delivers tasks best-effort, not blocking the heartbeat.

API (Admin Panel) ​

Prefix: /api/v1/d/cloudflare, Session admin auth. Package: internal/apps/openflare/cloudflare/; routes: internal/router/v1/openflare/register_cloudflare.go.

ResourceMethod & Path
ConnectionGET/PUT /connection, POST /connection/verify, POST /connection/clear
OverviewGET /overview
GroupsGET/POST /groups, GET /groups/:id, `POST /groups/:id/update
MembersGET/POST /groups/:id/members, `POST .../members/:memberId/update
Available domainsGET /domains/available
  • Success response.OK; failure response.Abort*; the Token is never returned in JSON.
  • Handlers separated from logics.go; the CF client is abstracted behind an interface for replaceability.

Frontend ​

  • Navigation: frontend/lib/navigation/openflare-nav.ts adds Cloudflare → /cloudflare (near Website Management / DNS Accounts).
  • Routes:
    • /cloudflare: overview; guide to configure when not ready
    • /cloudflare/settings: mixed Token config and test connection
    • /cloudflare/groups, /cloudflare/groups/[id]: list and detail (members, orange-cloud, sync)
  • Services: independent service under frontend/lib/services/openflare/, extending BaseService.
  • Pages follow the existing title-bar and component-split conventions; destructive actions need double confirmation.
  • Copy that must be visible: sync overwrites module-managed A records; multiple same-name A records need manual cleanup; removal deletes remote records by default; phase 1 has no automatic failover.

Errors and Security ​

  • User-visible copy is module-internal constants; internal errors log via pkg/logger.
  • Typical: token not configured, invalid token, node without IP, no CF Zone, multiple same-name A records, rate limiting.
  • Token is only decrypted server-side for use; responses and logs must never contain plaintext tokens.

Data Migration ​

  • goose both dialects (PG/SQLite) create the three tables; defaults match Go zero values.

Key Decision Summary ​

DecisionConclusion
Module shapestandalone Cloudflare pointing module, not embedded Zone fields
Tokenmixed: imported from DNS account or encrypted standalone
Domain granularityZoneDomain (FQDN)
Record shapesingle A → active node IPv4
Failoverphase 2; heartbeat offline; phase 1 only stores backup/active
Orange-cloudmember-level effective; group default only initializes
SoTDB tables as desired state drive CF

Released under the Apache License 2.0