@c15t/scripts 3.0.0-alpha.1 → 3.0.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/AGENTS.md +129 -59
  2. package/README.md +8 -29
  3. package/SKILL.md +33 -0
  4. package/dist/adobe-analytics.js +2 -0
  5. package/dist/ahrefs-analytics.js +2 -0
  6. package/dist/amplitude.js +2 -0
  7. package/dist/clearbit.js +2 -0
  8. package/dist/cloudflare-web-analytics.js +2 -0
  9. package/dist/cloudflare-zaraz.js +2 -0
  10. package/dist/crisp.js +2 -0
  11. package/dist/databuddy.js +2 -0
  12. package/dist/e2e-test-utils.js +2 -137
  13. package/dist/engine/compile.js +2 -89
  14. package/dist/engine/runtime.js +2 -448
  15. package/dist/events.js +2 -0
  16. package/dist/fathom-analytics.js +2 -0
  17. package/dist/front-chat.js +2 -0
  18. package/dist/google-tag-manager.js +2 -0
  19. package/dist/google-tag.js +2 -0
  20. package/dist/heap.js +2 -0
  21. package/dist/hightouch.js +2 -0
  22. package/dist/hotjar.js +2 -0
  23. package/dist/intercom.js +2 -0
  24. package/dist/klaviyo.js +2 -0
  25. package/dist/linkedin-insights.js +2 -0
  26. package/dist/logrocket.js +2 -0
  27. package/dist/matomo-analytics.js +2 -0
  28. package/dist/meta-pixel.js +2 -0
  29. package/dist/microsoft-clarity.js +2 -0
  30. package/dist/microsoft-uet.js +2 -0
  31. package/dist/mixpanel-analytics.js +2 -0
  32. package/dist/one-dollar-stats.js +2 -0
  33. package/dist/openai-pixel.js +2 -0
  34. package/dist/pinterest-tag.js +2 -0
  35. package/dist/pirsch.js +2 -0
  36. package/dist/plausible-analytics.js +2 -0
  37. package/dist/posthog.js +2 -0
  38. package/dist/promptwatch.js +2 -0
  39. package/dist/reddit-pixel.js +2 -0
  40. package/dist/registry.js +2 -392
  41. package/dist/resolve.js +2 -33
  42. package/dist/rudderstack.js +2 -0
  43. package/dist/rybbit-analytics.js +2 -0
  44. package/dist/segment.js +2 -0
  45. package/dist/snapchat-pixel.js +2 -0
  46. package/dist/tiktok-pixel.js +2 -0
  47. package/dist/types.js +2 -16
  48. package/dist/umami-analytics.js +2 -0
  49. package/dist/vendors/_shared/attributes.js +2 -14
  50. package/dist/vendors/_shared/google-consent.js +2 -27
  51. package/dist/vendors/_shared/install-builders.js +2 -21
  52. package/dist/vendors/_shared/required-id.js +2 -0
  53. package/dist/vendors/_shared/script-url.js +2 -28
  54. package/dist/vendors/ads-and-pixels/linkedin-insights.js +2 -48
  55. package/dist/vendors/ads-and-pixels/meta-pixel.js +2 -153
  56. package/dist/vendors/ads-and-pixels/microsoft-uet.js +2 -110
  57. package/dist/vendors/ads-and-pixels/openai-pixel.js +2 -88
  58. package/dist/vendors/ads-and-pixels/pinterest-tag.js +2 -0
  59. package/dist/vendors/ads-and-pixels/reddit-pixel.js +2 -107
  60. package/dist/vendors/ads-and-pixels/snapchat-pixel.js +2 -87
  61. package/dist/vendors/ads-and-pixels/tiktok-pixel.js +2 -89
  62. package/dist/vendors/ads-and-pixels/x-pixel.js +2 -48
  63. package/dist/vendors/analytics/adobe-analytics.js +2 -49
  64. package/dist/vendors/analytics/ahrefs-analytics.js +2 -27
  65. package/dist/vendors/analytics/amplitude.js +2 -134
  66. package/dist/vendors/analytics/clearbit.js +2 -28
  67. package/dist/vendors/analytics/cloudflare-web-analytics.js +2 -32
  68. package/dist/vendors/analytics/databuddy.js +2 -103
  69. package/dist/vendors/analytics/fathom-analytics.js +2 -35
  70. package/dist/vendors/analytics/google-tag.js +2 -66
  71. package/dist/vendors/analytics/heap.js +2 -134
  72. package/dist/vendors/analytics/hightouch.js +2 -109
  73. package/dist/vendors/analytics/hotjar.js +2 -44
  74. package/dist/vendors/analytics/logrocket.js +2 -58
  75. package/dist/vendors/analytics/matomo-analytics.js +2 -191
  76. package/dist/vendors/analytics/microsoft-clarity.js +2 -100
  77. package/dist/vendors/analytics/mixpanel-analytics.js +2 -93
  78. package/dist/vendors/analytics/one-dollar-stats.js +2 -0
  79. package/dist/vendors/analytics/pirsch.js +2 -67
  80. package/dist/vendors/analytics/plausible-analytics.js +2 -81
  81. package/dist/vendors/analytics/posthog.js +2 -200
  82. package/dist/vendors/analytics/promptwatch.js +2 -29
  83. package/dist/vendors/analytics/rudderstack.js +2 -183
  84. package/dist/vendors/analytics/rybbit-analytics.js +2 -63
  85. package/dist/vendors/analytics/segment.js +2 -56
  86. package/dist/vendors/analytics/umami-analytics.js +2 -39
  87. package/dist/vendors/analytics/vercel-analytics.js +2 -53
  88. package/dist/vendors/email-and-sms/klaviyo.js +2 -0
  89. package/dist/vendors/functional/crisp.js +2 -100
  90. package/dist/vendors/functional/front-chat.js +2 -0
  91. package/dist/vendors/functional/intercom.js +2 -45
  92. package/dist/vendors/tag-managers/cloudflare-zaraz.js +2 -98
  93. package/dist/vendors/tag-managers/google-tag-manager.js +2 -59
  94. package/dist/vercel-analytics.js +2 -0
  95. package/dist/x-pixel.js +2 -0
  96. package/dist-types/adobe-analytics.d.ts +2 -0
  97. package/dist-types/ahrefs-analytics.d.ts +2 -0
  98. package/dist-types/amplitude.d.ts +2 -0
  99. package/dist-types/clearbit.d.ts +2 -0
  100. package/dist-types/cloudflare-web-analytics.d.ts +2 -0
  101. package/dist-types/cloudflare-zaraz.d.ts +2 -0
  102. package/dist-types/crisp.d.ts +2 -0
  103. package/dist-types/databuddy.d.ts +2 -0
  104. package/dist-types/e2e-test-utils.d.ts +2 -0
  105. package/dist-types/engine/compile.d.ts +2 -3
  106. package/dist-types/engine/runtime.d.ts +2 -3
  107. package/dist-types/events.d.ts +2 -0
  108. package/dist-types/fathom-analytics.d.ts +2 -0
  109. package/dist-types/front-chat.d.ts +2 -0
  110. package/dist-types/google-tag-manager.d.ts +2 -0
  111. package/dist-types/google-tag.d.ts +2 -0
  112. package/dist-types/heap.d.ts +2 -0
  113. package/dist-types/hightouch.d.ts +2 -0
  114. package/dist-types/hotjar.d.ts +2 -0
  115. package/dist-types/intercom.d.ts +2 -0
  116. package/dist-types/klaviyo.d.ts +2 -0
  117. package/dist-types/linkedin-insights.d.ts +2 -0
  118. package/dist-types/logrocket.d.ts +2 -0
  119. package/dist-types/matomo-analytics.d.ts +2 -0
  120. package/dist-types/meta-pixel.d.ts +2 -0
  121. package/dist-types/microsoft-clarity.d.ts +2 -0
  122. package/dist-types/microsoft-uet.d.ts +2 -0
  123. package/dist-types/mixpanel-analytics.d.ts +2 -0
  124. package/dist-types/one-dollar-stats.d.ts +2 -0
  125. package/dist-types/openai-pixel.d.ts +2 -0
  126. package/dist-types/pinterest-tag.d.ts +2 -0
  127. package/dist-types/pirsch.d.ts +2 -0
  128. package/dist-types/plausible-analytics.d.ts +2 -0
  129. package/dist-types/posthog.d.ts +2 -0
  130. package/dist-types/promptwatch.d.ts +2 -0
  131. package/dist-types/reddit-pixel.d.ts +2 -0
  132. package/dist-types/registry.d.ts +2 -458
  133. package/dist-types/resolve.d.ts +2 -9
  134. package/dist-types/rudderstack.d.ts +2 -0
  135. package/dist-types/rybbit-analytics.d.ts +2 -0
  136. package/dist-types/segment.d.ts +2 -0
  137. package/dist-types/snapchat-pixel.d.ts +2 -0
  138. package/dist-types/tiktok-pixel.d.ts +2 -0
  139. package/dist-types/types.d.ts +2 -314
  140. package/dist-types/umami-analytics.d.ts +2 -0
  141. package/dist-types/vendors/_shared/attributes.d.ts +2 -35
  142. package/dist-types/vendors/_shared/google-consent.d.ts +2 -47
  143. package/dist-types/vendors/_shared/install-builders.d.ts +2 -30
  144. package/dist-types/vendors/_shared/required-id.d.ts +2 -0
  145. package/dist-types/vendors/_shared/script-url.d.ts +2 -75
  146. package/dist-types/vendors/ads-and-pixels/linkedin-insights.d.ts +2 -92
  147. package/dist-types/vendors/ads-and-pixels/meta-pixel.d.ts +2 -289
  148. package/dist-types/vendors/ads-and-pixels/microsoft-uet.d.ts +2 -105
  149. package/dist-types/vendors/ads-and-pixels/openai-pixel.d.ts +2 -211
  150. package/dist-types/vendors/ads-and-pixels/pinterest-tag.d.ts +2 -0
  151. package/dist-types/vendors/ads-and-pixels/reddit-pixel.d.ts +2 -210
  152. package/dist-types/vendors/ads-and-pixels/snapchat-pixel.d.ts +2 -171
  153. package/dist-types/vendors/ads-and-pixels/tiktok-pixel.d.ts +2 -106
  154. package/dist-types/vendors/ads-and-pixels/x-pixel.d.ts +2 -183
  155. package/dist-types/vendors/analytics/adobe-analytics.d.ts +2 -75
  156. package/dist-types/vendors/analytics/ahrefs-analytics.d.ts +2 -62
  157. package/dist-types/vendors/analytics/amplitude.d.ts +2 -234
  158. package/dist-types/vendors/analytics/clearbit.d.ts +2 -60
  159. package/dist-types/vendors/analytics/cloudflare-web-analytics.d.ts +2 -67
  160. package/dist-types/vendors/analytics/databuddy.d.ts +2 -147
  161. package/dist-types/vendors/analytics/fathom-analytics.d.ts +2 -90
  162. package/dist-types/vendors/analytics/google-tag.d.ts +2 -93
  163. package/dist-types/vendors/analytics/heap.d.ts +2 -316
  164. package/dist-types/vendors/analytics/hightouch.d.ts +2 -285
  165. package/dist-types/vendors/analytics/hotjar.d.ts +2 -73
  166. package/dist-types/vendors/analytics/logrocket.d.ts +2 -101
  167. package/dist-types/vendors/analytics/matomo-analytics.d.ts +2 -41
  168. package/dist-types/vendors/analytics/microsoft-clarity.d.ts +2 -97
  169. package/dist-types/vendors/analytics/mixpanel-analytics.d.ts +2 -113
  170. package/dist-types/vendors/analytics/one-dollar-stats.d.ts +2 -0
  171. package/dist-types/vendors/analytics/pirsch.d.ts +2 -96
  172. package/dist-types/vendors/analytics/plausible-analytics.d.ts +2 -122
  173. package/dist-types/vendors/analytics/posthog.d.ts +2 -175
  174. package/dist-types/vendors/analytics/promptwatch.d.ts +2 -36
  175. package/dist-types/vendors/analytics/rudderstack.d.ts +2 -330
  176. package/dist-types/vendors/analytics/rybbit-analytics.d.ts +2 -82
  177. package/dist-types/vendors/analytics/segment.d.ts +2 -158
  178. package/dist-types/vendors/analytics/umami-analytics.d.ts +2 -93
  179. package/dist-types/vendors/analytics/vercel-analytics.d.ts +2 -66
  180. package/dist-types/vendors/email-and-sms/klaviyo.d.ts +2 -0
  181. package/dist-types/vendors/functional/crisp.d.ts +2 -78
  182. package/dist-types/vendors/functional/front-chat.d.ts +2 -0
  183. package/dist-types/vendors/functional/intercom.d.ts +2 -135
  184. package/dist-types/vendors/tag-managers/cloudflare-zaraz.d.ts +2 -39
  185. package/dist-types/vendors/tag-managers/google-tag-manager.d.ts +2 -94
  186. package/dist-types/vercel-analytics.d.ts +2 -0
  187. package/dist-types/x-pixel.d.ts +2 -0
  188. package/docs/README.md +129 -59
  189. package/docs/assets/v3/bottom-bar.png +0 -0
  190. package/docs/assets/v3/brand-card.png +0 -0
  191. package/docs/assets/v3/brand-preferences.png +0 -0
  192. package/docs/assets/v3/choice-wall.png +0 -0
  193. package/docs/assets/v3/headless-bar-html.png +0 -0
  194. package/docs/assets/v3/headless-bar-mobile.png +0 -0
  195. package/docs/assets/v3/headless-bar.png +0 -0
  196. package/docs/assets/v3/slim-bar.png +0 -0
  197. package/docs/concepts/choose-your-setup.md +87 -0
  198. package/docs/concepts/consent-categories.md +84 -0
  199. package/docs/concepts/consent-state.md +357 -0
  200. package/docs/{guides → concepts}/data-fetching.md +31 -27
  201. package/docs/concepts/how-consent-works.md +123 -0
  202. package/docs/concepts/policies.md +71 -0
  203. package/docs/customization/class-names.md +202 -0
  204. package/docs/customization/dark-mode.md +157 -0
  205. package/docs/customization/motion.md +119 -0
  206. package/docs/customization/overview.md +67 -33
  207. package/docs/customization/recipes.md +839 -47
  208. package/docs/customization/slots.md +216 -35
  209. package/docs/customization/stylesheets.md +147 -0
  210. package/docs/customization/tailwind.md +842 -0
  211. package/docs/customization/tokens.md +166 -36
  212. package/docs/customization/translations.md +61 -3
  213. package/docs/frameworks/astro/embeds.md +160 -0
  214. package/docs/frameworks/astro/network-blocker.md +86 -0
  215. package/docs/frameworks/astro/scripts.md +146 -0
  216. package/docs/frameworks/html/embeds.md +142 -0
  217. package/docs/frameworks/html/network-blocker.md +105 -0
  218. package/docs/frameworks/html/scripts.md +164 -0
  219. package/docs/frameworks/javascript/scripts.md +119 -0
  220. package/docs/frameworks/next/embeds.md +90 -0
  221. package/docs/frameworks/next/network-blocker.md +153 -0
  222. package/docs/frameworks/next/scripts.md +196 -0
  223. package/docs/frameworks/nuxt/embeds.md +81 -0
  224. package/docs/frameworks/nuxt/network-blocker.md +97 -0
  225. package/docs/frameworks/nuxt/scripts.md +89 -0
  226. package/docs/frameworks/react/embeds.md +89 -0
  227. package/docs/frameworks/react/network-blocker.md +140 -0
  228. package/docs/frameworks/react/scripts.md +115 -0
  229. package/docs/frameworks/svelte/embeds.md +96 -0
  230. package/docs/frameworks/svelte/network-blocker.md +141 -0
  231. package/docs/frameworks/svelte/scripts.md +137 -0
  232. package/docs/frameworks/sveltekit/embeds.md +103 -0
  233. package/docs/frameworks/sveltekit/network-blocker.md +149 -0
  234. package/docs/frameworks/sveltekit/scripts.md +141 -0
  235. package/docs/frameworks/tanstack-start/embeds.md +96 -0
  236. package/docs/frameworks/tanstack-start/network-blocker.md +145 -0
  237. package/docs/frameworks/tanstack-start/scripts.md +103 -0
  238. package/docs/frameworks/vue/embeds.md +84 -0
  239. package/docs/frameworks/vue/network-blocker.md +99 -0
  240. package/docs/frameworks/vue/scripts.md +93 -0
  241. package/docs/guides/banner-experiments.md +654 -0
  242. package/docs/guides/troubleshooting.md +120 -47
  243. package/docs/guides/verify-consent.md +81 -49
  244. package/docs/integrations/adobe-analytics.md +168 -160
  245. package/docs/integrations/ahrefs-analytics.md +153 -155
  246. package/docs/integrations/amplitude.md +163 -157
  247. package/docs/integrations/building-integrations.md +136 -37
  248. package/docs/integrations/clearbit.md +155 -155
  249. package/docs/integrations/cloudflare-web-analytics.md +157 -157
  250. package/docs/integrations/cloudflare-zaraz.md +210 -262
  251. package/docs/integrations/crisp.md +165 -159
  252. package/docs/integrations/databuddy.md +158 -174
  253. package/docs/integrations/fathom-analytics.md +159 -157
  254. package/docs/integrations/front-chat.md +322 -0
  255. package/docs/integrations/google-maps.md +119 -84
  256. package/docs/integrations/google-tag-manager.md +179 -164
  257. package/docs/integrations/google-tag.md +164 -161
  258. package/docs/integrations/heap.md +164 -156
  259. package/docs/integrations/hightouch.md +162 -158
  260. package/docs/integrations/hotjar.md +160 -156
  261. package/docs/integrations/intercom.md +184 -154
  262. package/docs/integrations/klaviyo.md +486 -0
  263. package/docs/integrations/linkedin-insights.md +175 -151
  264. package/docs/integrations/logrocket.md +161 -157
  265. package/docs/integrations/matomo-analytics.md +189 -179
  266. package/docs/integrations/meta-pixel.md +189 -151
  267. package/docs/integrations/microsoft-clarity.md +164 -156
  268. package/docs/integrations/microsoft-uet.md +149 -155
  269. package/docs/integrations/mixpanel-analytics.md +156 -161
  270. package/docs/integrations/one-dollar-stats.md +306 -0
  271. package/docs/integrations/openai-pixel.md +205 -302
  272. package/docs/integrations/overview.md +143 -83
  273. package/docs/integrations/pinterest-tag.md +329 -0
  274. package/docs/integrations/pirsch.md +170 -160
  275. package/docs/integrations/plausible-analytics.md +173 -159
  276. package/docs/integrations/posthog.md +227 -242
  277. package/docs/integrations/promptwatch.md +155 -155
  278. package/docs/integrations/reddit-pixel.md +186 -158
  279. package/docs/integrations/rudderstack.md +202 -187
  280. package/docs/integrations/rybbit-analytics.md +172 -161
  281. package/docs/integrations/segment.md +183 -155
  282. package/docs/integrations/snapchat-pixel.md +187 -157
  283. package/docs/integrations/tiktok-pixel.md +172 -151
  284. package/docs/integrations/umami-analytics.md +164 -159
  285. package/docs/integrations/vercel-analytics.md +167 -158
  286. package/docs/integrations/x-pixel.md +177 -151
  287. package/docs/integrations/youtube.md +122 -87
  288. package/docs/upgrade-v3.md +490 -354
  289. package/package.json +12 -236
  290. package/dist-types/__tests__/helpers.d.ts +0 -141
  291. package/docs/assets/v3/brand-bar.png +0 -0
  292. package/docs/assets/v3/mobile-card.png +0 -0
  293. package/docs/assets/v3/preferences.png +0 -0
  294. package/docs/frameworks/javascript/script-loader.md +0 -94
  295. package/docs/frameworks/next/script-loader.md +0 -210
  296. package/docs/frameworks/react/script-loader.md +0 -63
  297. package/docs/guides/consent-state.md +0 -60
  298. package/docs/guides/deployment-modes.md +0 -75
  299. package/docs/integrations/clear-on-revocation.md +0 -167
  300. package/docs/integrations/granular-consent.md +0 -208
@@ -0,0 +1,357 @@
1
+ ---
2
+ title: Consent state reference
3
+ description: How c15t saves choices, gates IAB vendors, hydrates server records
4
+ and keeps browser tabs and storage in step.
5
+ group: concepts
6
+ ---
7
+
8
+ This reference covers the edge cases behind the model in
9
+ [how consent works](./how-consent-works.md). You need it when you
10
+ build custom consent UI, run c15t across subdomains, or debug a choice that
11
+ changes between tabs.
12
+
13
+ ## Gate IAB vendors on the TC string
14
+
15
+ Under an IAB policy, a script, network rule or iframe that names a `vendorId`
16
+ or IAB purposes is an IAB target. It runs only while a confirmed TC string
17
+ grants what it declares: purpose and vendor consent for `iabPurposes`, purpose
18
+ and vendor legitimate interest for `iabLegIntPurposes`, and opt-ins for
19
+ `iabSpecialFeatures`. Every category it names must also be free of
20
+ restrictions, so GPC or strict scope blocks it.
21
+
22
+ A refused category is the one exception. It does not block an IAB target that
23
+ processes only on legitimate interest, after publisher restrictions, because
24
+ the TCF lets that processing run without consent. The visitor's control for it
25
+ is the objection, which the legitimate interest signals record. The category
26
+ itself stays refused: its `effectivePermissions` entry is `false`, and targets
27
+ that name only the category, or that also declare a consent purpose or special
28
+ feature, stay blocked.
29
+
30
+ ## When a choice is saved
31
+
32
+ A save records the choice in the browser first and sends it to the backend
33
+ afterwards. The stock banner, preference dialog and IAB surfaces in every
34
+ framework adapter, and the browser client's `acceptAll()`, `rejectAll()`
35
+ and `save()`, close without waiting for the backend. In order:
36
+
37
+ 1. In the click task, the explicit choice and effective permissions change,
38
+ `onChoiceRecorded` and `onPermissionsChanged` run, gated scripts, iframes
39
+ and network rules follow the new permissions, and the surface leaves the
40
+ active state. Its exit animation still plays.
41
+ 2. In the next task, the choice is written to the cookie and localStorage.
42
+ 3. After that write is queued, the request to the backend starts.
43
+ 4. When the request settles, the kernel emits `command:save:completed`. The
44
+ promise returned by `kernel.commands.save()`, React's `performAction()` and
45
+ Svelte's `saveConsents()` resolves or rejects only then.
46
+ 5. If the save turned off a category or vendor that was granted, the page
47
+ reloads in the next task, after `onBeforeConsentRevocationReload` runs.
48
+ Removing a script cannot stop code that already ran, so the reload starts a
49
+ page with only permitted code. With several saves in flight, it waits for
50
+ the last one. Set `reloadOnConsentRevoked: false` to handle revocation
51
+ yourself.
52
+
53
+ A failed request does not reopen the surface or roll the choice back. The
54
+ kernel emits `command:error`, which reaches the `onError` callback in adapters
55
+ that accept one, and queues the payload in localStorage. The queue is replayed
56
+ after the next successful initialization and when the browser comes back
57
+ online, up to 10 attempts over 7 days. A replay carries the original action
58
+ time and policy snapshot token, so the backend records when the visitor
59
+ decided, and a duplicate submission resolves to the same consent record. A
60
+ backend that signs policy snapshot tokens rejects a replay made after the
61
+ token expires, which is 30 minutes by default for the self-hosted backend. A
62
+ save still queued by then is recorded only in the browser.
63
+
64
+ IAB surfaces close in the click task too, but an IAB choice is recorded only
65
+ after its TC string is encoded, which can wait for the TCF library to load.
66
+ If that local step records nothing, for example because the vendor list
67
+ failed to load, the surface comes back so the visitor can try again.
68
+
69
+ ## Preserve records during hydration
70
+
71
+ Server helpers return records with policy information and evaluation time.
72
+ Forward that configuration intact. Copying an allowed category into a receipt
73
+ would invent a grant and lose its original confirmation time.
74
+
75
+ Valid v2 records can be read without a startup rewrite. The next explicit action
76
+ writes the v3 format. See [migration](../upgrade-v3.md) for expiry, partial saves,
77
+ custom transports and backend contract changes.
78
+
79
+ ## Keep open tabs in step
80
+
81
+ Tabs and windows on the same origin (scheme, host and port) share the consent
82
+ cookie and localStorage. When a visitor rejects in one tab, every other open tab
83
+ on that origin applies the rejection without a reload. Scripts and features
84
+ gated on `effectivePermissions` lose permission, subscribers are notified once
85
+ and `onPermissionsChanged` fires. Clearing records in one tab returns the others
86
+ to the active policy's defaults: an opt-in policy denies optional categories and
87
+ shows the prompt again.
88
+
89
+ Tabs on another subdomain that shares the consent cookie do not share
90
+ localStorage, so the browser sends them no `storage` event. They pick up the
91
+ change on their next focus or visibility change, or when you reconcile
92
+ yourself as described in
93
+ [Reconcile yourself or turn it off](#reconcile-yourself-or-turn-it-off).
94
+
95
+ The `storage` event only exists for localStorage. When localStorage is
96
+ unavailable (blocked by the browser, a sandboxed frame or a privacy mode) and
97
+ c15t stores in the cookie alone, another tab's change arrives only on the next
98
+ focus or visibility change, or when you call `runtime.reconcileStorage()` or
99
+ `persistence.reconcile()`. c15t does not poll the cookie or use a
100
+ `BroadcastChannel` for this.
101
+
102
+ Browser persistence reads stored records again at these moments:
103
+
104
+ | Moment | What happens |
105
+ | ------------------------------------------------------------------------ | --------------------------------- |
106
+ | Another tab or window on the same origin changes a c15t localStorage key | Reconciles on the `storage` event |
107
+ | The page becomes visible again | Reconciles on `visibilitychange` |
108
+ | The window regains focus | Reconciles on `focus` |
109
+ | The network reconnects | No storage read |
110
+
111
+ Several triggers in quick succession run one reconciliation in a later task.
112
+ Reconnecting does not read storage, because going online changes nothing in
113
+ browser storage. On reconnect the kernel retries saves the backend did not
114
+ accept and a failed initialization. Save retries do not write the cookie or
115
+ localStorage. A write that follows initialization obeys the rules in
116
+ [Ordering with pending writes](#ordering-with-pending-writes).
117
+
118
+ Every adapter that mounts browser persistence does this: the React, Next.js and
119
+ TanStack Start providers, Vue, Svelte, Astro, the script tag and
120
+ `createConsentRuntime`. With `persistence: false` nothing is stored or read.
121
+
122
+ ### What a reconciliation applies
123
+
124
+ Stored records are merged into the ones in memory:
125
+
126
+ * Category decisions merge per category. Each category keeps the decision with
127
+ the newer confirmation time, so a tab that only changed marketing never
128
+ reverts another tab's newer measurement decision.
129
+ * The notice dismissal and the vendor record are single decisions. A stored one
130
+ at least as new as the one in memory replaces it; an older one is ignored.
131
+ * When two tabs record decisions in the same millisecond, the one stored first
132
+ wins in both tabs.
133
+ * Tabs keep one subject. A stored subject is considered only when the record
134
+ that carries it changed, never on focus alone. It replaces a subject the tab
135
+ generated on its first save or copied from storage, so a tab that opened
136
+ before another tab stored a subject joins it. A subject id the server
137
+ resolved (at init, from a prefetch or in a save response) is replaced only by
138
+ a strictly newer stored choice, and an identity set with `identify()` is
139
+ never replaced.
140
+ * Under an IAB policy, the TC string follows the reconciled choice. See
141
+ [How the TC string is reconciled](#how-the-tc-string-is-reconciled).
142
+ * A record removed from readable storage since this tab last saw it present is
143
+ cleared, and the active policy decides again. A record this tab never saw in
144
+ storage, such as a receipt merged from the server after `identify()` or a
145
+ choice seeded while storage was blocked, stays.
146
+ * Blocked storage, or bytes that do not decode, change nothing. A failed read
147
+ never grants a category.
148
+
149
+ Expiry is not decided here. The applied records are evaluated at the time of the
150
+ read, the same way as at startup.
151
+
152
+ ### How the TC string is reconciled
153
+
154
+ Under an IAB policy, a tab adopts the TC string another tab stored, unless that
155
+ TC string conflicts with the reconciled choice. A conflicting TC string is
156
+ withdrawn, and `__tcfapi` reports no consent until the next save. Vendors never
157
+ receive a stale TC string: c15t withdraws it, or holds it back, before
158
+ `__tcfapi` publishes.
159
+
160
+ 1. The `@c15t/iab` module loads the TC string the other tab stored, with its
161
+ purpose, vendor and special-feature selections, so `__tcfapi` and the
162
+ preference controls show the choice now in force. Selections the visitor
163
+ changed in this tab without saving are kept.
164
+ 2. A missing or older stored TC string leaves the current one in place, unless
165
+ the current one conflicts with the reconciled choice. Then it is withdrawn.
166
+ 3. A category denied after the TC string was saved conflicts if the TC string
167
+ grants any of its purposes. A partial selection saved through IAB, which
168
+ records its category as denied because not every purpose is granted, keeps
169
+ its TC string.
170
+ 4. A TC string confirmed before the choice's newest decision no longer
171
+ describes it and is withdrawn, unless a newer receipt replaces it. That
172
+ receipt is adopted even when its TC string is identical, because TC strings
173
+ round their time to the day and custom-vendor selections live only in the
174
+ receipt. The receipt's expiry then applies.
175
+ 5. The TC string and its receipt (`euconsent-v2`, `c15t-iab-authority-v1`)
176
+ belong to one origin. After a save on a sibling subdomain that shares the
177
+ consent cookie, this subdomain reports no consent through `__tcfapi` until
178
+ it saves again.
179
+ 6. A tab reloads the TC string when another tab on the same origin stores a
180
+ new one. This covers a save in the same millisecond, or one that changed
181
+ only vendors.
182
+ 7. When two tabs save in the same millisecond with different selections, the
183
+ more restrictive TC string wins in both, so a revoked vendor is never
184
+ advertised again. If each grants something the other denies, neither is
185
+ published until the next save, and the stored receipt is removed so a page
186
+ opened later does not restore it.
187
+ 8. When another tab removes the receipt or clears localStorage, this tab
188
+ withdraws its TC string too, because a page opened now would find none. A
189
+ receipt this tab was still decoding is not installed.
190
+ 9. The removal after a tie checks that the stored receipt is the one it read.
191
+ localStorage has no conditional removal, so a receipt another tab stored a
192
+ moment earlier can still be removed. Every tab then withdraws its TC string
193
+ until the next save, so the race only ever withholds consent.
194
+
195
+ ### Clearing records across tabs
196
+
197
+ `clearRecords()` removes every record and then stores the time of the clear,
198
+ the clear epoch, under its own key: `c15t-epoch` in localStorage and a cookie
199
+ of the same name (`<storageKey>-epoch` with a custom `storageKey`). Clearing
200
+ never removes it. Every consent record written afterwards also records the
201
+ epoch it was written under.
202
+
203
+ A decision confirmed before the epoch was made before the clear, so it is void
204
+ wherever it turns up. A decision stamped in the very millisecond of the clear
205
+ counts only if it comes from a tab that had already seen that clear, so a tab's
206
+ own choice right after its own clear stands while another tab's decision in the
207
+ same millisecond does not. A stored record left with no decisions after the
208
+ clear is void too, subject included.
209
+
210
+ * A tab that reconciles only after another tab cleared and saved again drops
211
+ its pre-clear decisions instead of merging them back. Its subject from before
212
+ the clear is dropped too.
213
+ * A tab that missed the clear writes only decisions it made after it. Its queued
214
+ write of an earlier decision is discarded, so it cannot bring back a cleared
215
+ record.
216
+ * Browser hydration and server reads (`readStoredRecordsFromCookieHeader`, used
217
+ by the Next.js, TanStack Start, Nuxt, SvelteKit and Astro helpers) apply the
218
+ same rule, so a server render agrees with the browser.
219
+
220
+ Records from before any clear, including v2 and legacy records, read as epoch 0
221
+ and are unaffected. A corrupt epoch, or one that cannot be read, also reads as
222
+ 0: it voids nothing, so a failed read never grants a category. A consent record
223
+ whose own epoch field is corrupt is kept, and only its epoch is ignored.
224
+
225
+ Each clear moves the epoch forward, even when the device clock went back, so a
226
+ later clear never lets earlier decisions back in. Two clears in the same
227
+ millisecond therefore leave the epoch a millisecond ahead, and a decision saved
228
+ in that millisecond is void. An epoch up to one hour ahead
229
+ of the clock is kept, as it is when the clock was set back after a clear. Until
230
+ the clock catches up, decisions saved in that window are void too. An epoch more
231
+ than an hour ahead is treated as corrupt and reads as 0, so a clear never writes
232
+ one: after the clock went back more than an hour, the new epoch is capped at an
233
+ hour ahead of the clock.
234
+
235
+ That cap is a known limit. A cleared record carries times from before the clock
236
+ went back, and a runtime that missed the clear can write those times back. The
237
+ capped epoch is lower than them, so once the clock has recovered, such a
238
+ decision counts again. Leaving the epoch uncapped does not help: every tab
239
+ whose clock is still behind reads it as corrupt, which voids nothing. Times
240
+ alone cannot order a clear against decisions stamped by a clock that went back
241
+ more than an hour.
242
+
243
+ This changes the stored format. After a clear, the consent cookie carries
244
+ `&e=<time>` (16 bytes) and the localStorage record an `epoch` field (22 bytes),
245
+ and the epoch cookie itself holds a 13-digit time. Visitors who never cleared
246
+ their records store exactly what they did before. An older c15t build rejects
247
+ both the cookie and the localStorage record once they carry the epoch, so a page
248
+ still running one treats the visitor as undecided. Under an opt-out policy that
249
+ page grants optional categories by default until a new choice is saved, and the
250
+ configured prompt may appear again. Deploy the new build to every page of the
251
+ site before visitors can clear their records.
252
+
253
+ ### When the cookie and localStorage disagree
254
+
255
+ When both copies hold a decision for a category from the same millisecond and
256
+ the two conflict, the denial wins.
257
+
258
+ The subject and IAB metadata come from the cookie. A server response, such as
259
+ server-side consent restoration, and a sibling subdomain sharing the cookie
260
+ with `crossSubdomain` can both rewrite it without touching this origin's
261
+ localStorage, so the local copy can be the older one. The local copy's subject
262
+ is used only when this browser's last consent write reached localStorage but
263
+ not the cookie, for example because the cookie grew past the size limit, and
264
+ the cookie has not changed since. Such a write stores the cookie as it stood
265
+ under `<storageKey>-cookie-miss` in localStorage; the next write that reaches
266
+ the cookie, or a clear, removes it. When localStorage rejects a write that the
267
+ cookie takes, for example because storage is full, the older localStorage copy
268
+ is removed.
269
+
270
+ When a server render seeded the page from the consent cookie
271
+ (`skipHydration`), that seed stays authoritative. A denial that reached only
272
+ localStorage is still applied on top of it when the page mounts, since it can
273
+ only restrict, if it is newer than the seeded decision or from the same
274
+ millisecond as a seeded grant. A stored grant is not.
275
+
276
+ The notice dismissal and vendor denials are stored twice as well. A vendor
277
+ list in localStorage at least as new as the cookie's adds its denials but never
278
+ lifts one the cookie holds; a copy confirmed before the last clear is ignored, so its
279
+ denials never come back. The newer notice dismissal applies; it still only hides a
280
+ notice with the fingerprint it names. The consent record follows the rules
281
+ below.
282
+
283
+ The consent record is stored twice: as a cookie, which a server render reads,
284
+ and in localStorage. The cookie is authoritative. A well-formed cookie wins
285
+ even when it has expired, so a local copy can never bring back a grant the
286
+ cookie no longer carries.
287
+
288
+ A browser can still drop a cookie write, for example when the record grows past
289
+ the cookie size limit, while localStorage takes it. A denial in the local copy
290
+ that is newer than the cookie's decision for that category is therefore applied
291
+ on top of the cookie. A newer local grant is not, so a dropped cookie write only
292
+ ever leaves the visitor with less permission. A server render sees only the
293
+ cookie; after a dropped write that carried a denial, the browser is the stricter
294
+ of the two.
295
+
296
+ When the two copies were written under different clear epochs, each loses its
297
+ decisions from before the later epoch first, and the same rule applies to what
298
+ remains. A later epoch in the local copy never lets its grant replace a cookie
299
+ denial. The subject and IAB metadata come from the copy written under the later
300
+ epoch; a record written before the clear in force keeps its later decisions but
301
+ no subject.
302
+
303
+ A server render cannot know about a clear that never reached a cookie. If the
304
+ page could write localStorage but its cookie writes failed during
305
+ `clearRecords()` (cookies blocked for the page, or a cookie setter that
306
+ throws), the removal of the consent cookie failed too, and so did the epoch
307
+ cookie. The browser then applies the clear from localStorage, while a server
308
+ render still reads the old consent cookie until the next successful cookie
309
+ write. The same applies to a consent cookie set with a different `domain` than
310
+ the current `storageConfig` uses, which the clear cannot remove.
311
+
312
+ ### Ordering with pending writes
313
+
314
+ A tab writes its own choices in a later task, not during the click. Before it
315
+ reads storage, it lands its own queued writes, so a reconciliation never undoes
316
+ the visitor's latest action in that tab. A queued write follows the same rules
317
+ as a read: it stores the per-category merge of its choice and the stored one,
318
+ and never replaces a newer notice or vendor record. It keeps the stored subject
319
+ unless this tab identified a different user.
320
+ When a slow save response returns a server subject id, the tab adds it only to
321
+ the record it wrote; it does not recreate records another tab cleared or
322
+ overwrite another tab's newer choice.
323
+
324
+ Two tabs that write at the same moment can both read storage before either
325
+ writes, and the later write can then drop the other tab's category decision.
326
+ The tab whose decision was dropped still holds it, and on its next
327
+ reconciliation it writes it back, merged with what storage holds. It does so
328
+ only for decisions it actually stored itself, never for one a clear voided or
329
+ another tab replaced with a newer one.
330
+
331
+ If another tab's change reaches this tab while one of its saves is pending, the
332
+ save depends on how far it got. A save not yet sent is dropped. A request
333
+ already sent still reaches the backend, which may record it; this tab ignores
334
+ the response, so it queues no retry and applies no subject id from it. A save
335
+ already queued for retry keeps its original decision time.
336
+
337
+ ### Reconcile yourself or turn it off
338
+
339
+ Browsers send no event for a change made in the same document, or for a cookie
340
+ rewritten without a localStorage change, such as a `Set-Cookie` response header
341
+ or another subdomain sharing the cookie. The next focus or visibility change
342
+ picks it up. To apply it at once, call the method yourself:
343
+
344
+ ```ts
345
+ // Runtime owners: returns true when any record changed.
346
+ runtime.reconcileStorage();
347
+
348
+ // Kernel owners with createPersistence from c15t/modules/persistence:
349
+ persistence.reconcile();
350
+ ```
351
+
352
+ To keep storage but stop automatic reconciliation, pass `sync: false` in the
353
+ persistence options, for example `createConsentRuntime({ persistence: { sync:
354
+ false } })` or `createPersistence({ kernel, sync: false })`. The manual methods
355
+ still work.
356
+
357
+ `dispose()` removes the listeners and cancels a scheduled reconciliation.
@@ -1,23 +1,17 @@
1
1
  ---
2
- title: Data fetching and transports
3
- description: Choose cached manifests, backend init or offline policy resolution,
4
- and understand where consent records are saved.
5
- group: guides
2
+ title: Data fetching
3
+ description: How c15t gets policy data through a cached manifest, backend /init,
4
+ the browser or offline rules, and where consent choices are saved.
5
+ group: concepts
6
6
  ---
7
7
 
8
- ## Start with Inth and a cached manifest
8
+ ## Compare the fetching paths
9
9
 
10
- Use Inth for managed policy and consent records. For a Next.js application with
11
- a server, use a cached manifest to resolve policy in your application. Route
12
- browser consent traffic directly to Inth, or optionally use a Next.js rewrite
13
- to keep those requests on your app's origin. Follow the
14
- [Next.js manifest setup](https://c15t.com/docs/frameworks/next/data-fetching).
15
-
16
- Backend ownership and data fetching are separate decisions. Inth manages the
17
- backend for you. A [self-hosted backend](https://c15t.com/docs/self-host/quickstart) uses the same
18
- protocol while you operate its database, policies and availability. A static
19
- site can still call Inth. Only `offline()` deliberately removes consent backend
20
- requests and stores choices locally.
10
+ Where policy resolves and where choices are saved are separate from who runs
11
+ the backend. Inth and a [self-hosted backend](https://c15t.com/docs/self-host/quickstart) speak
12
+ the same protocol, so every path below works with either. Only `offline()`
13
+ removes backend requests. For a recommendation by framework, start with
14
+ [choose your setup](./choose-your-setup.md).
21
15
 
22
16
  | Fetching path | Where policy resolves | Where choices are submitted | Choose it when |
23
17
  | ------------------------------ | ------------------------------------------------------------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------ |
@@ -44,6 +38,16 @@ Do not put secrets, visitor identifiers or consent records into a manifest.
44
38
  Keep personalized init responses out of shared caches. Changing a policy also
45
39
  requires a refresh strategy for cached or build-time manifests.
46
40
 
41
+ ## How manifest mode counts visitors
42
+
43
+ The backend counts visitors from `/init` requests, and manifest resolution
44
+ skips them. To keep the count, the server adapters send a session report after
45
+ each resolution: the host posts to the backend's `POST /sessions` from the
46
+ server, after the response. The browser sends nothing, and the report stores
47
+ no identity; the visitor's IP address and user agent are forwarded under the
48
+ backend's usual IP handling. Static output resolves in the browser and sends
49
+ no report. Set `reportSessions: false` on an adapter to turn it off.
50
+
47
51
  ## What does regular `/init` do?
48
52
 
49
53
  `hosted({ url })` uses `${url}/init` for initialization and `${url}/subjects` for
@@ -54,7 +58,7 @@ can belong to Inth or your own c15t backend.
54
58
  import { hosted } from 'c15t';
55
59
 
56
60
  export function createConsentMode(backendURL: string) {
57
- return hosted({ url: backendURL });
61
+ return hosted({ url: backendURL });
58
62
  }
59
63
  ```
60
64
 
@@ -81,9 +85,9 @@ separate policy reads and record writes. With a backend rewrite mounted at
81
85
  import { hosted } from 'c15t';
82
86
 
83
87
  export const mode = hosted({
84
- url: '/api/c15t',
85
- initURL: '/api/c15t/init',
86
- assertDecisionInputs: true,
88
+ url: '/api/c15t',
89
+ initURL: '/api/c15t/init',
90
+ assertDecisionInputs: true,
87
91
  });
88
92
  ```
89
93
 
@@ -105,11 +109,11 @@ and TLS connection to the consent backend. The app server still connects to
105
109
  the upstream backend for manifest refreshes and consent writes. Vendor scripts
106
110
  and vendor requests keep their own origins.
107
111
 
108
- Set the absolute upstream endpoint through `C15T_BACKEND_URL` in Next.js server
109
- configuration. The endpoint URL is public connection information, not a secret.
110
- The browser uses `/api/c15t` without needing the upstream URL. A static export
111
- cannot serve a Next.js route or rewrite at runtime; use the absolute Inth URL or
112
- a proxy provided by the static host instead.
112
+ The rewrite destination and the init route use the absolute upstream endpoint,
113
+ such as `https://your-project.inth.app`. The browser uses `/api/c15t` without
114
+ needing the upstream URL. A static export cannot serve a Next.js route or
115
+ rewrite at runtime; use the absolute Inth URL or a proxy provided by the static
116
+ host instead.
113
117
 
114
118
  ## When should I use offline mode?
115
119
 
@@ -134,7 +138,7 @@ export const mode = offline();
134
138
  With no `policyRules`, the current offline transport uses the recommended rule
135
139
  pack. Supplying `policyRules` replaces that pack. Unknown country and region are
136
140
  real resolution inputs; offline mode does not discover a visitor's location.
137
- Use [policy rules](https://c15t.com/docs/frameworks/next/concepts/policy-presets) to understand
141
+ Use [policy rules](./policies.md) to understand
138
142
  matching and defaults, and test the missing-location case.
139
143
 
140
144
  Offline mode is an explicit architecture choice, not an automatic fallback for
@@ -160,4 +164,4 @@ refresh. In the recommended Next.js setup, the browser should call only
160
164
  upstream destinations.
161
165
 
162
166
  Test different locations, missing location headers, GPC, returning choices and
163
- backend failure. See [verification](./verify-consent.md).
167
+ backend failure. See [verification](../guides/verify-consent.md).
@@ -0,0 +1,123 @@
1
+ ---
2
+ title: How consent works
3
+ description: What c15t decides on each page load, the difference between a
4
+ permission and a recorded choice, and what happens when a visitor saves.
5
+ group: concepts
6
+ ---
7
+
8
+ ## What c15t decides on each page load
9
+
10
+ On every page load c15t answers one question for each consent category: is
11
+ this allowed right now? To answer it, c15t:
12
+
13
+ 1. Selects a **policy** for the visitor, usually by country and region. The
14
+ policy says whether optional categories start denied (opt-in) or allowed
15
+ (opt-out), and whether to show a banner.
16
+ 2. Reads the visitor's **stored choice** from a cookie and localStorage, if
17
+ there is one that still matches the policy.
18
+ 3. Reads **privacy signals** such as Global Privacy Control (GPC).
19
+
20
+ The result is a set of **permissions**, one per category. Components, script
21
+ loaders, embeds and your own code read those permissions. Nothing optional
22
+ runs until the policy has resolved: while it loads, and if it fails, every
23
+ optional category is denied.
24
+
25
+ A banner is only the interface. It does not stop a script you load with a
26
+ plain `<script>` tag or a vendor SDK you initialize yourself. To gate that code,
27
+ register it with c15t. See [integrations](../integrations/overview.md).
28
+
29
+ ## Categories
30
+
31
+ | Category | Use it for |
32
+ | --------------- | -------------------------------------------------- |
33
+ | `necessary` | Code the site cannot work without. Always allowed. |
34
+ | `functionality` | Optional features such as support chat. |
35
+ | `measurement` | Analytics and usage measurement. |
36
+ | `experience` | Personalization. |
37
+ | `marketing` | Advertising, retargeting and pixels. |
38
+
39
+ Assign a category by what the code does, not by what would be convenient.
40
+ Calling analytics `necessary` does not make it necessary.
41
+ [Consent categories](./consent-categories.md) explains which
42
+ categories the preference dialog shows and how policy scope affects them.
43
+
44
+ ## Policies
45
+
46
+ A policy has a **model** and a **prompt**:
47
+
48
+ | Model | Optional categories before a choice |
49
+ | --------- | ----------------------------------------------------------------- |
50
+ | `opt-in` | Denied until the visitor allows them. |
51
+ | `opt-out` | Allowed until the visitor refuses them or sends a privacy signal. |
52
+ | `iab` | Controlled by an IAB TCF consent string. |
53
+ | `none` | Allowed, with no prompt. |
54
+
55
+ | Prompt | What the visitor sees |
56
+ | -------- | ------------------------------------------------------------------------------ |
57
+ | `choice` | A banner asking for a decision. |
58
+ | `notice` | A notice they can dismiss. Dismissing records an acknowledgement, not consent. |
59
+ | `none` | Nothing on load. The policy can still require a way to open preferences. |
60
+
61
+ With [Inth](https://inth.com) you manage policies in your project, and the app
62
+ receives them from the backend. Changing a stylesheet or importing a preset in
63
+ the browser does not override a hosted policy.
64
+ [Policies](./policies.md) covers presets, scope, and why a banner may
65
+ not appear.
66
+
67
+ ## A permission is not a recorded choice
68
+
69
+ These two values answer different questions, and mixing them up is the most
70
+ common integration bug:
71
+
72
+ * **Permission** (`effectivePermissions`, `useConsent('measurement')`) answers
73
+ "may this run now?" Under an opt-out policy it can be `true` before the
74
+ visitor has done anything.
75
+ * **Recorded choice** (`explicitChoice`) answers "what did the visitor decide?"
76
+ It only changes when the visitor accepts, rejects or saves preferences.
77
+
78
+ | Task | Read |
79
+ | ---------------------------------------------------- | ------------------- |
80
+ | Load a script or render an optional feature | Permission |
81
+ | Show what the visitor chose, or report consent rates | Recorded choice |
82
+ | Decide whether a prompt is needed | `promptRequirement` |
83
+ | Find out why a region behaves differently | `policyRule` |
84
+ | Check that the policy loaded | `resolution.status` |
85
+
86
+ Never turn a permission into a saved choice. Hydrating server state, reading a
87
+ cookie or rendering a component does not count as a visitor action.
88
+ `onChoiceRecorded` fires only for a visitor's action; `onPermissionsChanged`
89
+ also fires when a choice expires or a privacy signal changes.
90
+
91
+ ## Notices and privacy signals
92
+
93
+ Dismissing a notice acknowledges it. It grants nothing and leaves earlier
94
+ refusals in place.
95
+
96
+ GPC is a browser setting that asks sites not to sell or share data. c15t reads
97
+ it live on every evaluation and applies the restrictions your policy configures
98
+ for it. It never becomes a stored refusal: when the browser stops sending GPC,
99
+ the restriction ends. The backend also sees the current value.
100
+
101
+ ## What happens when a visitor saves
102
+
103
+ When a visitor clicks Accept, Reject or Save, c15t:
104
+
105
+ 1. Updates permissions and closes the banner or dialog in the same click.
106
+ Scripts, embeds and network rules follow the new permissions immediately.
107
+ 2. Writes the choice to the cookie and localStorage.
108
+ 3. Sends the choice to the consent backend. If the request fails, the choice
109
+ stays saved in the browser and the request is retried later.
110
+ 4. Reloads the page if the visitor turned off something they had allowed.
111
+ Code that already ran cannot be unloaded, so the reload starts a page with
112
+ only permitted code. Set `reloadOnConsentRevoked: false` to handle revocation
113
+ yourself.
114
+
115
+ The [consent state reference](./consent-state.md) documents the exact
116
+ ordering, retries, server hydration and how open tabs stay in step.
117
+
118
+ ## Where consent data comes from
119
+
120
+ A consent backend supplies policies and stores consent records. Use Inth, run
121
+ the [c15t backend](https://c15t.com/docs/self-host/overview) yourself, or keep policies in the
122
+ browser for local development. [Choose your setup](./choose-your-setup.md)
123
+ walks through the options for your framework and hosting.
@@ -0,0 +1,71 @@
1
+ ---
2
+ title: Policies
3
+ description: How policy models, prompts and scope decide what c15t asks
4
+ visitors, where to change the rules, and why a banner may not appear.
5
+ group: concepts
6
+ ---
7
+
8
+ ## What the policy controls
9
+
10
+ A policy rule selects the permission model, categories, prompt and persistent
11
+ privacy controls for a visitor. Your app reads the resolved rule and effective
12
+ permissions. It should not choose a different rule just to hide a banner.
13
+
14
+ | Policy setting | What your app should expect |
15
+ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
16
+ | `model: 'opt-in'` | Optional categories in scope wait for consent. |
17
+ | `model: 'opt-out'` | Categories in scope can be allowed before a choice. Saved refusals and privacy signals can restrict them. |
18
+ | `model: 'none'` | Categories in scope are allowed without an initial prompt. Stock consent controls stay hidden unless the rule grants privacy rights. |
19
+ | `prompt: 'choice'` | The banner asks for a choice when the current state requires one. |
20
+ | `prompt: 'notice'` | Dismissing the notice records an acknowledgement, not a consent grant. |
21
+ | `prompt: 'none'` | No automatic prompt. The rule can still require a way to open preferences. |
22
+ | `scopeMode: 'strict'` | Optional categories outside the rule's scope stay denied. |
23
+
24
+ Keep a persistent preferences entry point wherever the policy provides that
25
+ right. A visitor who dismissed a notice or rejected tracking still needs a way
26
+ to revisit their choice. Stock components read the rule's rights; custom UI
27
+ must do the same.
28
+
29
+ Use `effectivePermissions` to gate a script or embed. A `true` value under an
30
+ opt-out rule does not mean the visitor clicked Accept. Read `explicitChoice`
31
+ when you need the visitor's recorded decision. See
32
+ [how consent works](./how-consent-works.md#a-permission-is-not-a-recorded-choice) for these distinctions.
33
+
34
+ ## Where to change the rules
35
+
36
+ For Inth, configure policy rules on your hosted project. The app receives those
37
+ rules through the configured [data-fetching path](./data-fetching.md). Importing
38
+ `recommendedPolicyRules()` or changing browser styling does not override the
39
+ hosted policy.
40
+
41
+ For a backend you operate, use the
42
+ [policy packs guide](https://c15t.com/docs/self-host/guides/policy-packs). It covers
43
+ `manifest.policyRules`, preset selection and custom matchers. Browser-only
44
+ setups own their rules locally; see
45
+ [choose your setup](./choose-your-setup.md).
46
+
47
+ Presets are starting configurations. Select them for your actual processing,
48
+ and review their assumptions before allowing optional categories by default.
49
+ A country match does not establish a legal basis for a vendor's data use.
50
+
51
+ ## Why the banner may be absent
52
+
53
+ A missing banner can be an expected policy outcome or an initialization problem.
54
+ Check `resolution.status` before interpreting the active model.
55
+
56
+ | State | What to check |
57
+ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
58
+ | A policy matched and `promptRequirement.kind` is `none` | The rule does not ask for an initial prompt, or the stored records already satisfy it. Check persistent preferences separately. |
59
+ | A policy matched but a category is denied | Inspect `effectivePermissions`, `explicitChoice`, `privacySignals` and `restrictions`. A saved refusal or GPC can keep it denied. |
60
+ | No policy has matched | Check backend connectivity, location inputs and policy configuration. Optional categories stay denied while resolution is unresolved. |
61
+
62
+ Do not treat `policyRule` alone as proof that a policy resolved. The snapshot
63
+ also carries a safe rule for evaluation while resolution is pending or failed.
64
+ Use `resolution.policy` only after checking for `status: 'matched'`.
65
+
66
+ The local `recommendedPolicyRules()` pack uses opt-in rules for Europe and
67
+ Québec, opt-out for its supported US privacy states, and a no-prompt `none`
68
+ default for other known locations. An unknown country uses its strict opt-in
69
+ fallback. A US country with a missing state gets the US opt-out fallback.
70
+ These are the local pack's defaults, not a promise about your Inth project.
71
+ Browser-only mode does not discover a visitor's country from their IP address.