# Why we deprecated offline mode in c15t

*c15t 3.2 deprecates offline() and renames it local(), for development and tests only. Here is why consent needs a record that lives outside the browser.*

Engineering | 2026-11-02T15:27:49.720Z

In c15t 3.2, we deprecated offline mode. We renamed `offline()` to `local()`, and it's now for development and tests only. We didn't remove it.

Offline mode saved each visitor's consent choice in their browser and nowhere else. You didn't need a backend. So it was the quickest way to get a banner on your site.

But it never kept a record of consent. It was the wrong thing to offer for production.

Here's what changed, why we did it and what to use instead.

## What changed

- We deprecated `offline()`. It still works as an alias until c15t v4.
- `local()` replaces it. It does the same thing. Policy rules resolve in the browser, and choices stay in browser storage.
- In development builds, local mode logs one warning per page load. It tells you that c15t keeps no server-side record of consent. You won't see it in production builds. And it never blocks the banner or a save.
- If you decide a browser-only record is enough, like in tests, set `acknowledgeNoRecords: true`. That hides the warning.
- The script tag file `c15t.offline.js` is now `c15t.local.js`. We still ship the old file as a deprecated alias.
- `c15t setup` now labels local mode "Development and tests only, no consent records".
- In c15t v4, the `offline()` name goes away. And `acknowledgeNoRecords: true` becomes required in the `local()` type.

On an older major? c15t 1.9.0 and 2.4.0 add the same development warning to offline mode. They keep the `offline` name.

## Why we made this change

A consent banner has two jobs. First, it asks your visitor for a choice. Second, it keeps proof of that choice.

Offline mode only did the first job. The choice lived in browser storage on your visitor's device. c15t didn't keep a copy.

So if your visitor cleared their browser, the record was gone. And if someone later asked what a visitor agreed to and when, you had nothing to show them.

Under the General Data Protection Regulation (GDPR), the proof is on you. If your business relies on consent, you have to show it. Here's [Article 7(1)](https://eur-lex.europa.eu/eli/reg/2016/679/oj):

> “Where processing is based on consent, the controller shall be able to demonstrate that the data subject has consented to processing of his or her personal data.”

Regulation (EU) 2016/679, Article 7(1).

[Article 5(2)](https://eur-lex.europa.eu/eli/reg/2016/679/oj) makes it a general rule. The GDPR calls it accountability:

> “The controller shall be responsible for, and be able to demonstrate compliance with, paragraph 1 (‘accountability’).”

Regulation (EU) 2016/679, Article 5(2).

The European Data Protection Board (EDPB) explains what this means in its [Guidelines 05/2020 on consent](https://www.edpb.europa.eu/system/files/documents/files/file1/edpb_guidelines_202005_consent_en.pdf). They say: "The burden of proof will be on the controller". They also give an online example:

> “a controller could retain information on the session in which consent was expressed, together with documentation of the consent workflow at the time of the session, and a copy of the information that was presented to the data subject at that time. It would not be sufficient to merely refer to a correct configuration of the respective website.”

EDPB Guidelines 05/2020 on consent, version 1.1, paragraph 108.

## What the GDPR does not say

To be clear, the GDPR doesn't say "use a database". The EDPB says so: "The GDPR does not prescribe exactly how this must be done."

And it isn't illegal to keep consent only in the browser. Some sites keep their own records another way.

But a record that only lives on your visitor's device is weak proof. Your visitor can delete it any time. You can't read it or audit it later.

With offline mode, it was hard or impossible to show consent.

Offline mode also had no geolocation. So every visitor got the same policy.

It looked like a complete setup. It wasn't. We think it was irresponsible for c15t to make that the easy path, so we changed it.

## What a consent record holds

In c15t, a consent record is the backend's copy of a choice. It holds three things: which categories your visitor allowed or refused, when they chose and which policy and copy they saw.

That's close to what the EDPB asks for. And it's no more than they ask for. They say controllers "should have enough data to show a link to the processing (to show consent was obtained) but they shouldn't be collecting any more information than necessary."

## Two ways to keep consent records

In production, send each choice to a c15t backend. You've got two options.

### Inth hosted

[Inth](https://inth.com) is the backend we recommend for c15t. You create a project and set your policy rules. Then you add your site's origin to its trusted origins and copy the backend URL.

Sign-up is free. Inth keeps a record of every choice, and you don't run a database.

### Self-hosted

Or you can run [`@c15t/backend`](https://c15t.com/docs/self-host/overview) on your own server, with your own SQL database. Your app connects to it with the same modes it uses for Inth.

The catch is that you run it. You set up the database. You own migrations, backups, policy configuration, secrets, uptime and upgrades.

Start with the [self-hosted quickstart](https://c15t.com/docs/self-host/quickstart).

Already have your own consent store? A [`custom()` transport](https://c15t.com/docs/concepts/data-fetching#can-i-provide-my-own-transport) can send each choice to your own API.

## How to move off offline mode

In a React app built with Vite, change the mode and set the backend URL:

```ts
// Before
import { offline } from 'c15t/react';
const mode = offline();

// After, for production
import { hosted } from 'c15t/react';
const mode = hosted();
```

`hosted()` reads the backend URL from `VITE_C15T_BACKEND_URL` or `VITE_INTH_PROJECT_URL`. Other frameworks use their own variable. You'll find each one in the [consent modes docs](https://c15t.com/docs/concepts/modes).

Do you use the script tag? Load `c15t.js` with `data-backend-url` instead of `c15t.offline.js`.

Keep `local()` for tests and demos:

```ts
import { local } from 'c15t/react';
const mode = local({ acknowledgeNoRecords: true });
```

## Thank you

Offline mode was our call. So is this change. If it breaks something for you, or you've got questions, [open an issue on GitHub](https://github.com/c15t/c15t/issues).

## Keep reading

[All articles](/blog)