You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/api.md
+3-1Lines changed: 3 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -12,14 +12,16 @@ Visit your Sink instance at `https://your-domain/_docs/scalar` for interactive A
12
12
13
13
## Authentication
14
14
15
-
All API endpoints require authentication via Bearer token in the `Authorization` header:
15
+
All API endpoints accept the site token in the `Authorization` header:
16
16
17
17
```http
18
18
Authorization: Bearer YOUR_SITE_TOKEN
19
19
```
20
20
21
21
The token is the same as `NUXT_SITE_TOKEN` configured in your environment variables.
22
22
23
+
When [Cloudflare Access authentication](cloudflare-access.md) is configured, browser requests from an authenticated dashboard session can use the signed Access application token instead. Sink validates the token signature, issuer, audience, and expiration. Clients must not construct or trust `Cf-Access-Jwt-Assertion` themselves; Cloudflare Access supplies the token or authorization cookie.
Sink can optionally use Cloudflare Access as an alternative to the existing site token. When Access is not configured, authentication behaves exactly as before.
4
+
5
+
With Access configured, an API request is accepted when either condition is true:
6
+
7
+
- The request has a valid `NUXT_SITE_TOKEN` bearer token.
8
+
- The request has a valid Cloudflare Access application JWT.
9
+
10
+
Sink verifies the Access JWT signature, issuer, audience, and expiration against your team's public keys. The presence of an Access header or cookie alone is never trusted.
11
+
12
+
## Compatibility-first setup
13
+
14
+
This setup protects the dashboard while keeping public short links and SiteToken API clients unchanged.
15
+
16
+
1. Create a Cloudflare Access self-hosted application for your Sink hostname.
17
+
2. Configure its application path to cover both `/dashboard` and its child routes.
18
+
3. Do not protect `/api` with Access. Sink authenticates API requests itself using SiteToken or the signed Access application cookie.
19
+
4. In the Access application's advanced cookie settings:
20
+
- Keep **Cookie Path** disabled so the dashboard cookie is also sent to `/api`.
21
+
- Set **SameSite** to `Lax` or `Strict` when your deployment does not require cross-site requests.
22
+
5. Add the following Sink environment variables and redeploy:
Both variables are required. The team domain should not have a path. The AUD tag is available in the Access application's additional settings.
30
+
31
+
Short-link paths, static assets, and API documentation remain public at the Cloudflare Access layer. API operations still require Sink authentication. Protect `/_docs` separately if the API schema should not be public.
32
+
33
+
## Security considerations
34
+
35
+
In compatibility-first mode, `/api` is not evaluated by the Cloudflare Access proxy on every request. Sink validates the signed application JWT locally. As a result, an Access session revoked by an administrator may remain usable until its JWT expires. Use an appropriately short Access policy or application session duration.
36
+
37
+
Access uses a browser cookie, so Sink rejects cross-site browser requests authenticated through Access and verifies the `Origin` header for state-changing methods. SiteToken requests are unchanged. Non-browser clients should continue to use `NUXT_SITE_TOKEN`.
38
+
39
+
Do not expose an alternative deployment hostname with a weak SiteToken. Cloudflare Access on the dashboard does not protect other hostnames that route to the same Worker or Pages project.
40
+
41
+
## Logout
42
+
43
+
When the dashboard is authenticated through Access, Sink redirects logout to `/cdn-cgi/access/logout`. Cloudflare revokes the Access session across applications and clears the application cookie.
44
+
45
+
## Strict setup
46
+
47
+
For stronger edge enforcement, you can protect both `/dashboard` and `/api` with Access. In this mode, Cloudflare blocks requests before they reach Sink, so a SiteToken-only API client cannot use the protected hostname. Such clients must also use an Access service token or a separate API hostname.
Copy file name to clipboardExpand all lines: docs/configuration.md
+55Lines changed: 55 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -99,6 +99,16 @@ This feature requires:
99
99
100
100
Backups are stored in R2 with the path `backups/links-{timestamp}.json` and run daily at 00:00 UTC.
101
101
102
+
## `NUXT_CF_ACCESS_TEAM_DOMAIN`
103
+
104
+
Optional Cloudflare Access team domain, for example `https://your-team.cloudflareaccess.com`.
105
+
Set this together with `NUXT_CF_ACCESS_AUD` to allow a valid Cloudflare Access session to authenticate API requests as an alternative to `NUXT_SITE_TOKEN`.
106
+
107
+
## `NUXT_CF_ACCESS_AUD`
108
+
109
+
Optional Application Audience (AUD) tag from the Cloudflare Access application that protects the dashboard.
110
+
Cloudflare Access authentication is enabled only when both Access variables are configured. Refer to [Cloudflare Access Authentication](cloudflare-access.md) for the required application and cookie settings.
111
+
102
112
## `NUXT_SAFE_BROWSING_DOH`
103
113
104
114
Set to a DNS over HTTPS (DoH) endpoint URL to enable automatic unsafe link detection when creating or editing links. When enabled, Sink queries the DoH service to check if the destination domain is flagged as malicious. If the domain resolves to `0.0.0.0`, the link is automatically marked as unsafe and visitors will see a warning page before being redirected.
@@ -114,3 +124,48 @@ Default is empty (disabled). Users can still manually mark links as unsafe in th
114
124
115
125
Optional custom redirect target when a slug is not found.
116
126
If this is not set, Sink will fall back to its default 404 page.
127
+
128
+
## Click Webhooks
129
+
130
+
Set `NUXT_WEBHOOK_URL` to send a best-effort webhook for each click included in access statistics. An empty URL disables webhooks. Bot clicks skipped by `NUXT_DISABLE_BOT_ACCESS_LOG` are also skipped by webhooks.
131
+
132
+
`NUXT_WEBHOOK_URL` must use HTTP or HTTPS. HTTPS is strongly recommended in production. `NUXT_WEBHOOK_SECRET` is optional. When configured, it must start with `whsec_`; the suffix is a Base64-encoded HMAC key between 24 and 64 bytes. Generate a 32-byte key with:
133
+
134
+
```sh
135
+
printf'whsec_%s\n'"$(openssl rand -base64 32)"
136
+
```
137
+
138
+
Sink sends a Dub-style payload:
139
+
140
+
```json
141
+
{
142
+
"id": "evt_...",
143
+
"event": "link.clicked",
144
+
"createdAt": "2026-07-11T12:00:00.000Z",
145
+
"data": {
146
+
"click": {
147
+
"id": "clk_...",
148
+
"timestamp": "2026-07-11T12:00:00.000Z",
149
+
"country": "US",
150
+
"region": "California",
151
+
"city": "San Francisco",
152
+
"device": "mobile",
153
+
"browser": "Mobile Safari",
154
+
"os": "iOS",
155
+
"referer": "example.com"
156
+
},
157
+
"link": {
158
+
"id": "link-id",
159
+
"slug": "example"
160
+
}
161
+
}
162
+
}
163
+
```
164
+
165
+
The click location fields contain the raw Cloudflare country code, region, and city. The device field prefers the parsed device category (such as `mobile`) and falls back to the device model.
166
+
167
+
Every request includes the Standard Webhooks headers `webhook-id` and `webhook-timestamp`. When a secret is configured, Sink also sends `webhook-signature`. The signature is `v1,<base64>` for HMAC-SHA256 over `<webhook-id>.<webhook-timestamp>.<raw-body>`, using the decoded secret suffix as the key. An invalid non-empty secret fails delivery and never falls back to unsigned delivery.
168
+
169
+
Without `NUXT_WEBHOOK_SECRET`, delivery is unauthenticated and unsigned. This mode is not recommended over untrusted networks; configure a secret whenever the receiver supports signature verification.
170
+
171
+
Webhook payloads exclude IP addresses, coordinates, full user agents, query parameters, passwords, and destination URLs. Delivery has a 10-second timeout, accepts only 2xx responses, does not follow redirects, and is asynchronous. Failures do not affect redirects and are not retried.
Copy file name to clipboardExpand all lines: docs/deployment/pages.md
+6Lines changed: 6 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -7,6 +7,8 @@
7
7
-`NUXT_SITE_TOKEN`: Must be at least **8** characters long. This token grants access to your dashboard.
8
8
-`NUXT_CF_ACCOUNT_ID`: Find your [account ID](https://developers.cloudflare.com/fundamentals/setup/find-account-and-zone-ids/).
9
9
-`NUXT_CF_API_TOKEN`: Create a [Cloudflare API token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) with at least `Account.Account Analytics` permission. [See reference.](https://developers.cloudflare.com/analytics/analytics-engine/sql-api/#authentication)
10
+
- (_Optional_) `NUXT_WEBHOOK_URL`: The HTTPS endpoint that enables and receives click webhooks.
11
+
- (_Optional_) `NUXT_WEBHOOK_SECRET`: A `whsec_`-prefixed Base64 secret. If omitted, delivery is unauthenticated and unsigned, which is not recommended over untrusted networks.
10
12
11
13
5. Save and deploy the project.
12
14
6. Cancel the deployment, then go to **Settings** -> **Bindings** -> **Add**:
@@ -22,3 +24,7 @@
22
24
- Go to **Settings** -> **Runtime** -> **Compatibility flags** and set the following flags `nodejs_compat`.
23
25
8. Redeploy the project.
24
26
9. To update code, refer to the official GitHub documentation [Syncing a fork branch from the web UI](https://docs.github.com/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork#syncing-a-fork-branch-from-the-web-ui'GitHub: Syncing a fork').
27
+
28
+
To optionally protect the dashboard with Cloudflare Zero Trust while keeping short links public, refer to [Cloudflare Access Authentication](../cloudflare-access.md).
29
+
30
+
Click webhook delivery is best effort and has no retries. See [Click Webhooks](../configuration.md#click-webhooks) for payload, signature, and privacy details.
Copy file name to clipboardExpand all lines: docs/deployment/workers.md
+6Lines changed: 6 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -14,7 +14,13 @@
14
14
-`NUXT_SITE_TOKEN`: Must be at least **8** characters long. This token grants access to your dashboard.
15
15
-`NUXT_CF_ACCOUNT_ID`: Find your [account ID](https://developers.cloudflare.com/fundamentals/setup/find-account-and-zone-ids/).
16
16
-`NUXT_CF_API_TOKEN`: Create a [Cloudflare API token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) with at least `Account.Account Analytics` permission. [See reference.](https://developers.cloudflare.com/analytics/analytics-engine/sql-api/#authentication)
17
+
- (_Optional_) `NUXT_WEBHOOK_URL`: The HTTPS endpoint that enables and receives click webhooks.
18
+
- (_Optional_) `NUXT_WEBHOOK_SECRET`: A `whsec_`-prefixed Base64 secret. Generate one with `printf 'whsec_%s\n' "$(openssl rand -base64 32)"` and store it as a secret in the dashboard, not in `wrangler.jsonc`. If omitted, delivery is unauthenticated and unsigned, which is not recommended over untrusted networks.
17
19
18
20
9. Enable Analytics Engine. In **Workers & Pages**, go to **Account details** in the right panel, locate **Analytics Engine**, and click **Set up** to enable the free tier. Name them `sink` and `ANALYTICS`, or else overwrite it with `NUXT_DATASET` and update your `wrangler.jsonc` accordingly
19
21
10. Redeploy the project.
20
22
11. To update your code, refer to the official GitHub documentation: [Syncing a fork branch from the web UI](https://docs.github.com/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork#syncing-a-fork-branch-from-the-web-ui'GitHub: Syncing a fork').
23
+
24
+
To optionally protect the dashboard with Cloudflare Zero Trust while keeping short links public, refer to [Cloudflare Access Authentication](../cloudflare-access.md).
25
+
26
+
Click webhook delivery is best effort and has no retries. See [Click Webhooks](../configuration.md#click-webhooks) for payload, signature, and privacy details.
0 commit comments