Cloudflare Worker companion for @structured-world/vue-privacy - GDPR consent storage in KV.
- Per-domain isolation (KV key prefix)
- 365-day TTL for consent storage
- Simple REST API (GET/POST)
- CORS support with per-domain configuration
- Rate limiting per IP address (configurable)
- Consent versioning for privacy policy updates
GET /api/consent?id=<user_id>&version=<expected_version>
Parameters:
id(required): User unique identifierversion(optional): Expected consent version. If provided and doesn't match stored version, returnsfound: falseto trigger re-consent
Response (consent found and version matches):
{
"found": true,
"consent": {
"categories": {
"analytics": true,
"marketing": false,
"functional": true
},
"timestamp": 1706198400000,
"version": "1",
"domain": "example.com",
"updatedAt": "2024-01-25T12:00:00.000Z"
}
}Response (version mismatch - triggers re-consent):
{
"found": false,
"versionMismatch": true,
"storedVersion": "1" // The version stored in KV (null for legacy consents without version)
}POST /api/consent
Content-Type: application/json
{
"id": "user-unique-id",
"categories": {
"analytics": true,
"marketing": false,
"functional": true
},
"version": "1"
}
Response:
{
"success": true,
"id": "user-unique-id"
}GET /api/geo
Response:
{
"isEU": true,
"countryCode": "DE",
"continent": "EU",
"region": "Bavaria",
"method": "worker"
}| Field | Type | Description |
|---|---|---|
isEU |
boolean | Whether visitor is in EU (for GDPR) |
countryCode |
string | null | ISO 3166-1 alpha-2 country code |
continent |
string | null | Continent code (EU, NA, AS, etc.) |
region |
string | null | Region/state name (for CCPA: California, Virginia, Colorado, Connecticut, Utah) |
method |
string | Always "worker" |
{domain}:{user_id}
Example: example.com:abc123-def456
The worker implements per-IP rate limiting to prevent abuse.
- 100 requests per minute per IP address
- Applies to
/api/consentendpoints only (GET and POST) /api/geois not rate-limited
Override defaults via wrangler.toml variables:
[vars]
RATE_LIMIT_MAX_REQUESTS = "200" # requests per window
RATE_LIMIT_WINDOW_SECONDS = "120" # 2 minute windowRate-limited endpoints (/api/consent) include rate limit headers:
| Header | Description |
|---|---|
X-RateLimit-Limit |
Maximum requests allowed per window |
X-RateLimit-Remaining |
Requests remaining in current window |
X-RateLimit-Reset |
Unix timestamp when window resets |
When limit is exceeded:
{
"error": "rate_limit_exceeded",
"retryAfter": 45
}Response headers include Retry-After with seconds until window resets.
Rate limit state is stored in KV with prefix rl::
rl:{ip_address}
This is a fixed-window rate limiter: the window resets based on an internal
resetAt timestamp. The KV entry TTL is set to windowSeconds on each allowed
request as a cleanup mechanism — entries auto-expire after the last allowed request.
Fail-open behavior: If KV operations fail (e.g., temporary unavailability), requests proceed without rate limiting. This prioritizes availability over strict enforcement. Rate limiting resumes automatically when KV recovers.
The worker supports consent versioning to handle privacy policy changes. When your privacy policy or cookie categories change, you can bump the consent version to invalidate existing consents and force users to re-consent.
- When storing consent via POST, include the
versionfield (e.g.,"1.0","2.0") - When retrieving consent via GET, pass the expected
versionquery parameter - If the stored version doesn't match the expected version, the response returns
found: falsewithversionMismatch: true - The client should show the consent banner again when version mismatch is detected
{
"found": false,
"versionMismatch": true,
"storedVersion": "1.0"
}- Use semantic versioning (e.g.,
"1.0","1.1","2.0") - Bump major version when cookie categories change
- Bump minor version for privacy policy text changes
- Store version in your app config and pass it to both GET and POST requests
// Your app config
const CONSENT_VERSION = "2.0"; // Bump when policy changes
// Check existing consent (relative URL works when served from same domain as the worker)
const response = await fetch(`/api/consent?id=${userId}&version=${CONSENT_VERSION}`);
const { found, versionMismatch } = await response.json();
if (!found) {
if (versionMismatch) {
console.log("Privacy policy updated, showing banner");
}
showConsentBanner();
}
// Store new consent
await fetch("/api/consent", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id: userId,
version: CONSENT_VERSION,
categories: { analytics: true, marketing: false, functional: true }
})
});- Fork this repository
- Create KV namespace:
wrangler kv:namespace create CONSENT_KV
- Update
wrangler.tomlwith your KV namespace ID - Add route for your domain:
routes = [ { pattern = "yourdomain.com/api/consent*", zone_name = "yourdomain.com" }, { pattern = "yourdomain.com/api/geo", zone_name = "yourdomain.com" }, ]
- Deploy:
yarn deploy
yarn install
yarn dev # Local development
yarn test # Run tests
yarn deploy # Manual deploy| Feature | Status |
|---|---|
| GET/POST consent API | Done |
| Per-domain KV isolation | Done |
| 365-day TTL storage | Done |
| CORS configuration | Done |
| GitHub Actions deploy | Done |
| Rate limiting | Done |
| Consent versioning | Done |
| Feature | Description |
|---|---|
| vue-privacy integration | Automatic sync with @structured-world/vue-privacy storage backend |
| Bulk export | Admin API for compliance exports |
| Analytics events | Optional consent analytics (opt-in rates) |
This worker is designed to work with the @structured-world/vue-privacy npm package
for server-side consent storage.
Note: Integration with vue-privacy is planned but not yet implemented. Currently the worker provides a standalone REST API for consent storage.
Apache 2.0