CDN Allowlist
The CDN allowlist (introduced in v0.15.0) tells Picora’s Markdown rewriter which http(s):// image URLs to leave alone — typically because they’re already hosted on a Picora-controlled CDN.
Without an allowlist entry, Picora can’t tell https://media.picora.me/abc.jpg from https://example.com/random.jpg — both look like third-party HTTP URLs and would be left untouched (Picora deliberately doesn’t re-host third-party images for legal reasons).
The allowlist is admin-managed because it affects all users globally.
What gets allowlisted
The allowlist primarily covers:
- Picora’s primary CDN domains:
media.picora.me,media.picora.cn - Picora’s video / audio domains:
video.picora.me,video.picora.cn - Picora’s MCP domains:
mcp.picora.me,mcp.picora.cn - Future Picora CDN regions: e.g.,
media-us.picora.me,media-asia.picora.me - Legacy domains: when migrating from old domains to new
- User-confirmed third-party CDNs that store Picora-served images: rare; only with admin review
Default seed entries
When a fresh database is initialized, the allowlist starts with three system-managed entries:
| Pattern | Match type | Note |
|---|---|---|
media.picora.me | exact | Default Picora CF media domain |
.picora.me | suffix | All *.picora.me subdomains |
.picora.cn | suffix | All *.picora.cn subdomains |
These cannot be deleted through the UI (the delete button is disabled with a tooltip). To remove a seed entry, you must directly modify the sys_cdn_whitelist table — but doing so risks breaking Markdown rewriting for all users; don’t.
Match types
Two match types are supported. Glob / regex are deliberately not supported (ReDoS risk).
exact
The URL’s hostname must equal the pattern, case-insensitive.
Pattern: media.picora.me✅ media.picora.me/abc.jpg✅ MEDIA.PICORA.ME/abc.jpg❌ cdn.media.picora.me❌ picora.me/abc.jpgsuffix
The URL’s hostname must end with the pattern. The pattern conventionally starts with a dot (e.g., .picora.me) to mean “any subdomain of picora.me”.
Pattern: .picora.me✅ media.picora.me✅ video.picora.me✅ asia.media.picora.me❌ picora.me (← exact match without subdomain not included; add a separate exact entry if needed)❌ notpicora.meSuffix matches are O(1) per entry (string comparison); the runtime cost of having 100+ entries is negligible.
Adding an entry
You’ll need an admin account (is_admin = 1 in auth_users).
- Open Settings → Admin → CDN Allowlist (admin-only nav item)
- Click Add entry
- Fill in:
- Pattern: hostname or
.suffix.example.comform - Match type: exact or suffix
- Note: optional but strongly recommended — explain why this domain is trusted
- Pattern: hostname or
- Click Save
The entry takes effect within 60 seconds globally (the API server holds the allowlist in memory and refreshes via SWR every 60s).
Editing / removing an entry
- User-added entries (created by an admin, not seed): can be soft-deleted via Revoke. Soft-delete sets
is_active = 0; the row stays in the table for audit purposes but no longer matches URLs. - Seed entries: cannot be removed through the UI. The delete button is disabled.
There’s no “edit” — to change a pattern, soft-delete the old entry and create a new one. This preserves audit trail.
Validation rules
The pattern field rejects:
- URLs (e.g.,
https://example.com) — must be hostname only - Paths (e.g.,
example.com/foo) — pattern can’t include path - Special chars (
*,?,[,], etc.) except dot - Leading dot for
exactmode (only allowed forsuffix)
Invalid patterns return 422 DOC_INVALID_PATTERN.
Use cases
Onboarding a new Picora CDN region
When Picora launches a new edge region, e.g., media-eu.picora.me:
- The new region’s hostname is already covered by the seed
.picora.mesuffix entry — no action needed - If you launch a region under a different TLD (rare), add a new entry then
Recovering from a botched rewrite
If a user uploaded a Markdown document with images already hosted at a domain that was not allowlisted at the time, Picora may have re-uploaded those images. To prevent recurrence:
- Identify the domain (check the document’s
failuresfield in upload response logs) - Add it to the allowlist (with a note explaining the source)
- Future uploads from that user will skip the rewrite
Cache behavior
The allowlist is cached in two places:
- API server in-memory: refreshed every 60 seconds (SWR)
- ICache key
cdn_whitelist_version: incremented on every change; servers detect the bump and refresh immediately
Worst case: a 60-second delay between admin action and global propagation. This is a deliberate trade-off — synchronous global invalidation would require a coordination layer that adds complexity and latency to every request.
Observability
Each allowlist operation emits structured logs:
| Event | Level | Fields |
|---|---|---|
cdn_whitelist.startup_loaded | info | entries_count, load_duration_ms |
cdn_whitelist.update | info | admin_id, action (add/remove), pattern |
cdn_whitelist.match | debug (sampled 1%) | url, matched_pattern |
cdn_whitelist.refresh_failed | warn (P2) | error |
See Observability for log routing and alerting.
Common issues
“My new entry isn’t taking effect” — wait 60 seconds for SWR refresh. If still not, check the API server logs for cdn_whitelist.refresh_failed.
“User reports duplicate images uploaded after Markdown rewrite” — the user’s source domain isn’t allowlisted. Add it.
“422 DOC_INVALID_PATTERN when adding entry” — pattern contains special chars or includes https:// / path. Strip down to bare hostname.
“Can’t delete seed entries” — by design. Modify directly in the database only as a last resort and follow change management procedures.
Related
- Markdown rewriting — how the allowlist interacts with the rewriter
- Observability
- Content moderation
- API Reference — Admin (coming v0.16.0)