Backend & Database · 7 min read · Updated 22 August 2026
How to store data in an AI-built app with Supabase
Apps built in NorthernGo do not need a database to be connected — every generated app already has a Supabase PostgreSQL database wired in through a global object called `window.NorthernGoDB`. You save a record with `await window.NorthernGoDB.save(collection, data)` and read it back with `await window.NorthernGoDB.get(collection)`. There is no schema to define and no connection string to paste.
The database is already there
The most common misunderstanding about AI app builders is that you have to bring your own backend. In NorthernGo you do not. Every app that gets generated has a database bridge injected into it before you ever see the code, connected to a shared Supabase PostgreSQL instance hosted in the EU.
That bridge is exposed as a single global object, window.NorthernGoDB. The AI agents that write your app already know about it and use it by default, which means that if you simply ask for "a task list that remembers what I typed", the generated app will already be persisting data properly. You only need to understand the API if you want to modify the code yourself, or if you want to know what is actually happening underneath.
There is no table to create in advance. Collections are created the first time you save something to them, which is deliberate — schema migration is exactly the kind of work that stops non-developers from finishing a project.
Step 1: Save your first record
save() takes a collection name and a plain object. The object can hold whatever shape you want; the collection does not have to exist yet.
await window.NorthernGoDB.save('tasks', {
text: 'Buy milk',
done: false,
createdAt: new Date().toISOString()
});
It returns a promise, so it belongs in an async function and should be wrapped in try/catch. A failed write with no error handling is the single most common reason a generated app appears to "lose" data — the record never arrived and nothing said so.
async function addTask(text) {
try {
await window.NorthernGoDB.save('tasks', { text, done: false });
await renderTasks();
} catch (err) {
console.error(err);
showMessage('Could not save. Check your connection and try again.');
}
}
Step 2: Read the data back
get() takes a collection name and resolves to an array of the records in it. Call it once when the page loads and then again after any write, so the interface always reflects what is actually stored.
window.addEventListener('DOMContentLoaded', async () => {
try {
const tasks = await window.NorthernGoDB.get('tasks');
renderTasks(tasks || []);
} catch (err) {
console.error(err);
renderTasks([]);
}
});
Note the || [] fallback. An empty collection can resolve to something other than an array, and rendering code that assumes an array will throw on a brand new app with no data in it yet. This is the second most common failure in generated apps, and it produces the worst symptom: a completely blank screen on first load.
Step 3: Delete records
delete() takes the collection plus a field name and the value to match, rather than an internal row id. That keeps it usable from ordinary app code where you generally know the thing you want to remove, not its database key.
await window.NorthernGoDB.delete('tasks', 'text', 'Buy milk');
If several records match the field and value, they are all removed — so match on something genuinely unique when it matters. Storing your own identifier at write time is the reliable approach:
const id = crypto.randomUUID();
await window.NorthernGoDB.save('tasks', { id, text, done: false });
await window.NorthernGoDB.delete('tasks', 'id', id);
Step 4: Give each user their own data
Everything above stores data at the app level: every visitor sees the same records. As soon as you want people to have private data, the app needs login, and the same object handles that too.
await window.NorthernGoDB.register(email, password);
await window.NorthernGoDB.login(email, password);
const isLoggedIn = !!window.NorthernGoDB.getToken();
window.NorthernGoDB.logout();
Authentication is deliberately email and password. Once a user is logged in, the platform stamps each secure save with their identity and get() returns only their rows. You do not write that filter yourself, and you must not set a _ngOwner field — the server overwrites it. The usual pattern is to check getToken() at startup and show either the login form or the app itself.
Shared collections among signed-in users (a team CRM, an internal board) opt out of isolation with { shared: true }. Public catalogues, menus and high-score lists use false so guests can read them without an account:
await window.NorthernGoDB.save('contacts', contact, { shared: true });
const allContacts = await window.NorthernGoDB.get('contacts', { shared: true });
await window.NorthernGoDB.save('products', product, false);
const catalogue = await window.NorthernGoDB.get('products', false);
An app admin (role: 'admin') sees every row in the app. That is the account created with setupAdmin.
One thing that is not optional: an app that has login must also have a visible way to delete the account. That is GDPR article 17, not a nice-to-have, and the SDK gives you a single call for it.
if (confirm('This permanently deletes your account and all your data. Continue?')) {
await window.NorthernGoDB.deleteAccount();
location.reload();
}
Do not build your own deletion route with fetch and do not try to clear the user by calling delete() in a loop. deleteAccount() removes the auth record and the associated data together; a hand-rolled version will reliably leave something behind.
Mirroring data to a system you control
There is a separate feature in the workspace called Custom Database, and it is worth being precise about what it does, because the name suggests something it is not.
It does not swap the built-in Supabase database for one of your own, and it does not take a Supabase connection string. It takes an HTTP endpoint. Once set, every write your apps make is also POSTed to that URL as JSON:
{
"projectId": "abc123",
"collection": "tasks",
"data": { "text": "Buy milk", "done": false },
"timestamp": "2026-08-15T09:12:44.000Z"
}
That makes it a mirror, not a replacement. It is genuinely useful for piping app data into your own warehouse, a Zapier or Make scenario, an internal CRM, or a Fortnox integration — but the app keeps reading from the built-in database either way. If you need a fully independent backend, the honest answer is to export the source code and repoint it yourself.
What this does not give you
Being clear about the boundaries saves more time than any feature list.
- No SQL. You cannot write joins, aggregate queries or views against these collections from app code. The API is deliberately four methods wide.
- No realtime subscriptions. There is no listener that fires when another user writes. If you need live updates, poll on an interval.
- Queued offline writes, last-known reads. On a published PWA,
save()/delete()without a network are queued in the SDK and replayed when online.get()returns the last successful list plus pending rows. Login, payments, e-mail and AI still fail without a connection. See the offline-first guide. - No schema enforcement. Nothing stops you saving
{ text: 'x' }to one record and{ title: 'x' }to the next. Consistency is on you.
Troubleshooting
Data saves but disappears on reload. Almost always the app is writing to localStorage instead of the database. Search the generated code for localStorage.setItem — if app data is going there, it lives on one device only and vanishes when the cache is cleared. Ask the AI to move the storage to window.NorthernGoDB.
A blank screen on first load. The render function received something that was not an array from an empty collection. Add the || [] fallback and wrap the load in try/catch.
Writes silently do nothing. Check that the call is awaited. window.NorthernGoDB.save(...) without await inside an async function will often complete after the page has already navigated or re-rendered.
Every user sees everyone's data. That is expected without login, and for collections saved with false (public) or { shared: true } (signed-in team data). Private records behind login are isolated automatically; if two customers still see each other, the generated code is passing false or shared: true on that collection.
Frequently asked questions
Do I need my own Supabase account to build an app with a database?
No. Every app generated in NorthernGo is already connected to a hosted Supabase PostgreSQL database in the EU through the window.NorthernGoDB object. There is no account to create, no connection string to paste and no table to define in advance.
Can I connect my own external database instead?
Not as a replacement. The Custom Database setting takes an HTTP endpoint and mirrors every write to it as JSON, so the data reaches your own system, but the app keeps reading from the built-in database. To run fully on your own backend you would export the source code and repoint it yourself.
Why does my app lose its data when I reload the page?
Almost always because the app is writing to localStorage rather than the database. Data in localStorage exists only in that one browser on that one device and disappears when the cache is cleared. Search the code for localStorage.setItem and move any app data over to window.NorthernGoDB.save.
Can two users see each other’s data?
Without login, yes — records are stored at app level and everyone sees the same collection. Adding register and login through window.NorthernGoDB stamps each secure save to that user, so the same get call returns only their own data. Pass { shared: true } for team collections, or false for a public catalogue.
Does the database work when the app is offline?
On a published PWA, yes for data: get() shows the last synced list plus queued saves, and save/delete replay when the app is online again. Login, payments, e-mail and in-app AI still need a connection. Pending rows are marked _pending: true.
Build it yourself
NorthernGo turns a plain-text description into a working web app with a database, login and a live URL. Local AI generation runs on your own GPU, is unlimited, and is free on every plan.