To add a UK GDPR cookie banner to a Base44 app, you need a banner where Reject all is as easy as Accept all, and code that stops anything optional running until someone says yes. The banner is the easy half: on my Base44 site, the hard half was Base44's own analytics, which logged page views and stored a visitor ID before anyone had clicked a thing.
The bit that made me wince
Before the fix, I tested my site the way a new visitor meets it: a clean browser, nothing clicked, no choice made. The old build sent 5 analytics requests to Base44 and stored an ID called base44_analytics_session_id in local storage. No consent. No banner. Just vibes.
- analytics requests before anyone clicks
- 5 → 0
- visitor IDs stored without consent
- 1 → 0
- gzipped banner chunk, only fetched when it's needed
- 4 KB
That's the whole lesson, really. A cookie banner that doesn't stop anything is just decoration. If the tracking has already fired by the time the card appears, the prettiest design in the world is theatre.
What the UK rules actually ask for
Two laws matter here. PECR covers storing or reading anything on someone's device. UK GDPR sets the standard that consent has to meet. The ICO pulls both together in its guidance on storage and access technologies (opens in a new tab), finalised on 29 April 2026.
The core rule is one sentence: "You must obtain prior consent to the UK GDPR standard for their use, unless an exception applies."
And it's not only cookies. The guidance covers web storage too, which it says is "also known as 'local storage'". Base44's ID lives there, so it counts. Here's how the ICO's lines map onto what my banner does:
What the ICO asks for
- Prior consent, unless an exception applies
- Make it "as easy to refuse consent as it is to accept"
- "Silence or inactivity doesn't qualify"
- Withdraw "with the same ease that they gave it"
What my banner does
- Nothing optional runs until a choice is made
- Accept all and Reject all, same size, side by side
- Closing the tab without clicking counts as no
- Cookie settings in the footer of every page
There are exceptions. Strictly necessary storage (remembering the choice itself, say) never needs consent. And since February, there's a new one for analytics.
The new analytics exception, and why I still ask
The Data (Use and Access) Act 2025 rewrote this part of PECR, and section 112 (opens in a new tab) has been fully in force since 5 February 2026. It added a "statistical purposes" exception: no consent needed when the storage's sole purpose is collecting statistics so you can improve your site. You still have to explain it and give people a simple, free way to object.
The ICO's page on the exceptions (opens in a new tab) is strict about the edges. The result has to be "aggregate statistical information that you cannot use to identify people", and it "does not allow you to monitor or track individual visitors". No advertising, and an analytics provider has to be a processor that only uses the data to help you.
So why does my Analytics toggle still start off? Because of that word "sole". Base44's SDK keeps one ID per browser in local storage and sends it with every signed-out request, and its own code comment says that's so the backend "can support anonymous agent access". That isn't statistics. And I can't check from the outside what Base44 does with the data.
I'd be leaning on an exception I can't prove I meet. Asking first costs me a few stats. Happy with that.
What my banner does
Live on this site now. Choose opens three rows: Necessary (locked on), Analytics and Embedded content. Both optional ones start off. A browser sending Global Privacy Control (opens in a new tab) counts as "reject all", and the banner doesn't show. The choice is saved with the date and a policy version under aj:consent, and crawlers never see the banner or load it.
Two builder details I care about. The card never grabs focus or blocks scrolling, but it sits first in the page, like GOV.UK's, so the first Tab press lands on it. And it's its own chunk, so returning visitors never download it unless they open Cookie settings.
Have a go with a mini version. Flip the categories and watch which requests would be allowed:
Consent lab: a mini cookie banner
A working copy of this site's cookie banner, with Accept all, Reject all and Choose, and switches for Analytics and Embedded content. A live list shows what each choice allows or blocks: Base44's page-view analytics, the base44_analytics_session_id it stores and the Cal.com booking calendar.
A Global Privacy Control switch shows the browser's signal overriding everything to reject. It is a sandbox: it never changes your real choice on this site.
Step 1: list everything your app stores
Before writing any banner code, open your site in a fresh browser profile. Watch DevTools (Application and Network) while you click around, and write down everything that isn't strictly needed.
Mine came down to two optional things: Base44's analytics (the page-view logs plus that stored ID) and the Cal.com calendar on /book, which sets its own cookies. Everything else was necessary, like the consent record itself and a spam guard on forms. Every item went into a table in my privacy notice.
Step 2: one tiny consent module
Everything else asks one small module a yes or no question. This is trimmed from my src/lib/consent.js (the real one wraps every storage call in try/catch, because storage can be blocked):
// src/lib/consent.js (trimmed)
export const CONSENT_KEY = 'aj:consent';
export const CONSENT_VERSION = 1; // bump it and everyone is asked again
export function getConsent() {
const rec = read(); // parsed aj:consent, or null if missing or an old version
if (rec) return { decided: true, analytics: rec.analytics === true, embeds: rec.embeds === true };
if (navigator.globalPrivacyControl === true) {
return { decided: true, analytics: false, embeds: false, source: 'gpc' };
}
return { decided: false, analytics: false, embeds: false };
}
export const hasConsent = (category) => getConsent()[category] === true;
export function setConsent({ analytics = false, embeds = false }, source = 'banner') {
const rec = { v: CONSENT_VERSION, at: new Date().toISOString(), analytics, embeds, source };
localStorage.setItem(CONSENT_KEY, JSON.stringify(rec));
if (!analytics) localStorage.removeItem('base44_analytics_session_id');
window.dispatchEvent(new CustomEvent('aj:consent-change', { detail: getConsent() }));
}The date and version are there for a reason. UK GDPR says you must "be able to demonstrate you have valid consent", and a record of what was agreed and when is how you do that.
Step 3: turn off Base44's page-view tracker
This is where it gets Base44-specific. Base44's analytics docs (opens in a new tab) say "Analytics is on by default, and nothing changes for an app unless you ask." They also say turning analytics off "is not the same as an empty dashboard", because page view tracking "runs separately, so page views still appear".
In my build, the page-view logger was an inline script the Base44 vite plugin adds to index.html when its analyticsTracker option is on. It posts every page view on load, with no consent check. My code lives in GitHub and syncs to Base44, so I switched it off in vite.config.js:
// vite.config.js
base44({
// Off: its inline script logged every page view without consent.
analyticsTracker: false,
// ...your other options stay as they were
}),Then I moved the same logging into my own src/lib/analytics.js: same request, same page names, sent only once hasConsent('analytics') is true.
The SDK has its own analytics too (an event queue and a heartbeat). createClient takes an analytics option with an enabled flag. I couldn't find it in the docs, but it's in the SDK's type definitions:
// src/api/base44Client.js (trimmed)
export const base44 = createClient({
appId,
analytics: { enabled: Boolean(appId) && hasConsent('analytics') },
});One catch: an SDK client can't switch its analytics on after it's created. So if someone says yes mid-visit, my code starts a second, analytics-only client. Saying no stops it and drops anything still queued.
Step 4: the sneaky one, the SDK's visitor ID
This one's easy to miss. Even with the SDK's analytics switched off, base44_analytics_session_id still got stored. Which is funny, because the SDK's own type definitions say that with analytics disabled, it "does not create an analytics session identifier".
The reason is in the SDK's request code. On every signed-out request (reading entities, the contact form, the public settings check) it attaches an X-Base44-Anonymous-Id header, whether analytics is on or not. To get that ID, it calls getAnalyticsSessionId(), which creates the ID in local storage if it isn't there. So every visit stored it, consent or not.
The fix is a small build-time edit in vite/base44Consent.js. Without analytics consent, the function returns the SDK's own in-memory fallback ID instead. It lives for one page view and never touches storage:
// @base44/sdk getAnalyticsSessionId(), after the build-time edit (trimmed)
export function getAnalyticsSessionId() {
if (typeof window === "undefined") {
return getFallbackSessionId();
}
// added: no consent, no stored ID
if (!(typeof window.__ajAnalyticsConsent === "function" && window.__ajAnalyticsConsent())) {
return getFallbackSessionId();
}
// ...the SDK's original code carries on from hereThe edit checks for an exact piece of the SDK's code first, and if an upgrade moves it, the build warns instead of failing. That matters, because Base44's GitHub bot updates packages by itself. On 28 September it bumped the SDK from 0.8.51 to 0.8.52. I re-checked. The edit still applies.
Step 5: gate anything embedded
The Cal.com calendar on /book makes zero requests to Cal.com until Embedded content is on. Until then, the page shows a note: "Calendar's on standby." One button, Load the calendar, records that choice and loads it. Switch it off later and the iframe goes.
Same pattern for any embed that sets its own cookies. YouTube, maps, booking tools, chat widgets. Gate them all.
Step 6: make changing your mind easy
Cookie settings sits in the footer of every page and reopens the card in Choose mode. Saying no to analytics deletes the stored ID straight away. My privacy page also has a "Clear this browser" button that wipes everything the site stored, consent included, so the banner asks again.
How I checked it
Here's the test, so you can run it on your own app:
Open a clean browser
A fresh profile or a private window, so nothing is stored yet.
Don't click anything
Watch Network and Application in DevTools. Count analytics requests and stored keys.
Press Accept all
Page views should start logging straight away, client-side navigation included.
Try the edges
With Global Privacy Control on: no banner, and a reject is saved. With a crawler user agent: no banner, nothing stored.
Repeat after every SDK update
Base44's bot bumps packages by itself, so re-test when it does.
On the live site on 28 September: 0 analytics requests and no stored ID before consent. After Accept all, the page view logged straight away, and so did the next page I clicked through to.
(What crawlers actually get from Base44 is its own story: why Google sees an empty page on your Base44 app.)
Consent-gated analytics was also one piece of the speed work in how I cut 91 KB of JavaScript from my Base44 site.
Your checklist
- Every stored item and optional request is listed, with who sets it
- Accept all and Reject all look the same and sit side by side
- Optional toggles start off, and ignoring the banner means no
- The Base44 vite plugin's page-view tracker is off, with your own consent-gated logger
- The SDK's analytics follows consent, and its visitor ID isn't stored without it
- Embeds wait for consent
- Global Privacy Control counts as reject
- A Cookie settings link on every page
- A privacy notice table that matches what the code does
- Re-test in a clean browser after every SDK update
FAQ
Does a Base44 app need a cookie banner?
Usually, yes. Base44's analytics is on by default, and its SDK stores base44_analytics_session_id in local storage. Under PECR, storage like that needs consent unless it's strictly necessary or fits an exception such as the statistical purposes one.
Does local storage count as a cookie under UK law?
Yes. The ICO's guidance covers storage and access technologies in general, and names web storage, "also known as 'local storage'", alongside cookies. The same consent rules apply.
Can I run analytics without consent in the UK now?
Sometimes. Since 5 February 2026, PECR has a statistical purposes exception for storage used only to collect statistics that help you improve your site, as long as you explain it and give people a simple, free way to object. It doesn't cover tracking individual visitors or advertising, and any analytics provider may only use the data to help you.
Should Global Privacy Control count as a reject?
The ICO's guidance on managing consent doesn't mention Global Privacy Control. I treat it as "reject all" anyway, because it's a clear signal from the person. They can still switch things on from Cookie settings.
Building something similar and want a second pair of eyes on it? Email me at hello@aatmanjain.com.

