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_nodeswitches 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 Capability | Relationship |
|---|---|
of_zones / of_zone_domains | Provide the pointable FQDN list; this module only references zone_domain_id |
of_nodes.ip | Source of A record content; recommended to restrict to edge nodes with valid IPv4 |
of_dns_accounts + sealSensitive | ACME DNS-01 already supports Cloudflare Token; this module can import the same account or store a Token standalone |
| lego Cloudflare provider | Only TXT/DNS-01; this module builds its own CF HTTP client for Zone/DNS Record APIs |
Core Model
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)
| Field | Description |
|---|---|
source | dns_account | standalone |
dns_account_id | references of_dns_accounts (type=cloudflare) when source=dns_account |
authorization | encrypted storage when source=standalone, payload shape {"api_token":"..."}, consistent with DNS accounts; the API never returns it |
status / verified_at | connectivity 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
| Field | Description |
|---|---|
name | display name |
primary_node_id | primary node |
backup_node_id | backup (nullable; phase 1 stores only) |
active_node_id | currently effective node; equals primary in phase 1; rewritten by phase 2 failover |
default_proxied | group default orange-cloud; only affects newly added members |
enabled | whether 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
| Field | Description |
|---|---|
group_id | owning group |
zone_domain_id | globally unique: one domain belongs to at most one group |
proxied | member orange-cloud (the only runtime basis) |
cf_zone_id / cf_record_id | Cloudflare cache for idempotent updates |
desired_ip / sync_status / last_error / synced_at | desired 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
- Member
proxied: the only basis written to CF during sync. - Group
default_proxied: copied toproxiedwhen a member is added. - 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:
| Item | Value |
|---|---|
| type | A |
| name | the ZoneDomain's FQDN |
| content | the group active_node's IPv4 |
| proxied | member proxied |
| ttl | forced 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
| Trigger | Behavior |
|---|---|
| Manual sync (all / group / member) | reconcile |
| Member added | initialize proxied, then enqueue sync |
| Member removed / group deleted | delete the remote A managed by this module by default (configurable keep) |
Primary node / active / member proxied changed | re-sync the corresponding scope |
| Node IP changed (heartbeat or manual) | enqueue members whose active_node_id points to that node |
| Token not ready | refuse sync |
Phase 1 does not do scheduled full reconciliation.
Reconcile (single member, idempotent)
- Resolve the CF Zone by the FQDN's registrable root domain, cache
cf_zone_id. - With a
cf_record_id, prefer Update; if stale, list byname+type=A. - 0 records → Create; exactly 1 → take over and Update; multiple → fail and tell the user to clean up in CF.
- Write back
cf_record_id,desired_ip,sync_status,synced_at/last_error. - 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 inbootstrap. - 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.
| Resource | Method & Path |
|---|---|
| Connection | GET/PUT /connection, POST /connection/verify, POST /connection/clear |
| Overview | GET /overview |
| Groups | GET/POST /groups, GET /groups/:id, `POST /groups/:id/update |
| Members | GET/POST /groups/:id/members, `POST .../members/:memberId/update |
| Available domains | GET /domains/available |
- Success
response.OK; failureresponse.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.tsadds 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/, extendingBaseService. - 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
| Decision | Conclusion |
|---|---|
| Module shape | standalone Cloudflare pointing module, not embedded Zone fields |
| Token | mixed: imported from DNS account or encrypted standalone |
| Domain granularity | ZoneDomain (FQDN) |
| Record shape | single A → active node IPv4 |
| Failover | phase 2; heartbeat offline; phase 1 only stores backup/active |
| Orange-cloud | member-level effective; group default only initializes |
| SoT | DB tables as desired state drive CF |