# How to store data in an AI-built app with Supabase

> Every app built in NorthernGo gets a Supabase PostgreSQL database automatically. Here is the exact API for saving, reading and deleting records, plus how to add per-user login.

Source: https://northerngo.com/resources/connect-supabase/
Language: en
Updated: 2026-08-22

---
**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.

```javascript
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.

```javascript
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.

```javascript
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.

```javascript
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:

```javascript
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.

```javascript
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:

```javascript
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](/resources/gdpr-compliant-app-building-eu/), not a nice-to-have, and the SDK gives you a single call for it.

```javascript
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:

```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](/resources/avoid-vendor-lock-in-ai-builders/) 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](/resources/offline-first-web-apps-pwa/).
- **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.

---

## Related

- [How to build a web app that still works without internet](https://northerngo.com/resources/offline-first-web-apps-pwa/)
- [Building GDPR-compliant apps: a practical checklist for EU founders](https://northerngo.com/resources/gdpr-compliant-app-building-eu/)
- [How to avoid vendor lock-in when building with an AI app builder](https://northerngo.com/resources/avoid-vendor-lock-in-ai-builders/)
- [How to export an AI-built app as a native iOS and Android project with Capacitor](https://northerngo.com/resources/export-native-ios-android-capacitor/)

---

NorthernGo is an AI-powered platform for building production-ready web apps with zero coding. Local AI generation via WebGPU is unlimited and free, and you own all generated source code. https://northerngo.com/
