Use cases · 10 min read · Updated 15 August 2026
Build internal tools with AI: inventory, requests and a dashboard
Internal tools are the fastest win with an AI app builder: one inventory list, one request form, one dashboard that reads the same collections. In NorthernGo you describe the job in text, voice or a sketch; three agents write vanilla JavaScript and Tailwind on a Supabase PostgreSQL database in the EU. Put login in front of every internal screen. Without it the URL is a public spreadsheet of your stock.
This is the job the builder is actually good at
A public product has to look finished. An internal tool has to be used on Tuesday. That difference is why inventory lists, request forms and small dashboards are the fastest win when you describe an app in ordinary language and let an AI app builder write it.
You already know the fields. The workshop knows which spare parts run out. The office knows which form people fill in on paper and then lose. Nobody needs a design system. They need a screen that saves, a screen that lists, and a number that is not a week out of date.
NorthernGo is shaped for that. Every app already has a Supabase PostgreSQL database in the EU, e-mail and password login, and a PWA so the shell still loads on a phone in the yard. You type, talk or sketch. Architect plans, Builder writes one HTML file in vanilla JavaScript and Tailwind, Critic reviews on Premium and Pro. The code is yours. ZIP export is on Premium and Pro.
The warning that belongs above the prompt
Internal data is still personal data, and it is still confidential. A stock list with supplier names, a request form with who asked for what, a dashboard of hours — none of that should be reachable by anyone who finds the URL.
Without login, every visitor reads and writes the same collections. That is the default. It is convenient in the editor and disastrous the moment you publish. The published address is a URL. URLs leak into group chats.
So the rule is blunt. If the tool is internal, the first line of the prompt is login. Gate every list, every save and every dashboard tile on window.NorthernGoDB.getToken(). Add a visible Delete my account button that calls deleteAccount() after a confirmation — any app with login must, including one that only staff use. Staff have the same article 17 rights as customers.
Do not ask the model for "only people in our company can see this" and stop there. There is no company directory, no magic-link domain restriction, no single sign-on. E-mail and password. You create the accounts. You decide who gets the link.
Step 1: Pick one job, not a platform
The failed internal-tool prompt is "build us an operations hub". The working one names a Tuesday problem.
Three jobs that fit:
- Inventory. A list of items with name, sku, location, quantity, minQuantity. A form to add or adjust. A filter for items at or below minimum.
- Request forms. A staff member files a request (parts, leave, a vehicle). A second screen shows open requests. Status moves from open to ordered to done.
- A dashboard. Not charts for their own sake. Three numbers drawn from the same collections: items below minimum, requests waiting, last update time.
Build one of those first. A dashboard with no collections behind it is a poster. A request form that does not write to the database is a PDF.
A prompt that works:
Internal tool for a small workshop. Login required on every screen. Three views behind the login: Inventory, Requests, Overview. Inventory items: id, name, sku, location, quantity, minQuantity. Saving an item with an existing sku updates quantity instead of inserting a duplicate. Requests: id, title, requestedBy, status, createdAt. Status is open, ordered, done. Overview shows count of items where quantity <= minQuantity, count of open requests, and the newest lastTouchedAt. Delete my account button calling window.NorthernGoDB.deleteAccount() after confirm.
That is enough for Architect. It is also enough for you to reject the first generation when it invents a fourth view called Analytics.
Step 2: Inventory as a list you can count
Quantity is a number. The model will store it as a string if you let it, and then "12" + 1 becomes "121". Say "quantity is a number" in the prompt, and coerce it in code.
async function saveItem(item) {
const all = (await window.NorthernGoDB.get('items')) || [];
const sku = String(item.sku || '').trim().toUpperCase();
const existing = all.find(row => String(row.sku || '').trim().toUpperCase() === sku);
const record = {
id: existing ? existing.id : crypto.randomUUID(),
name: item.name,
sku,
location: item.location || '',
quantity: Number(item.quantity) || 0,
minQuantity: Number(item.minQuantity) || 0,
lastTouchedAt: new Date().toISOString()
};
await window.NorthernGoDB.save('items', record);
return record;
}
function lowStock(items) {
return items.filter(row => Number(row.quantity) <= Number(row.minQuantity));
}
The database API is already there: save, get, delete, plus login. No table to create. || [] on every get, or the first morning with an empty store shows a blank page and someone reopens the paper list.
Duplicates here are as damaging as in a CRM. Two rows for the same SKU mean two quantities. Match on SKU, update, do not insert. The model will not infer that from "inventory".
Step 3: Request forms that actually go somewhere
A form that only lives in the browser is a sticky note. Save it.
async function submitRequest(title, requestedBy) {
await window.NorthernGoDB.save('requests', {
id: crypto.randomUUID(),
title: String(title || '').trim(),
requestedBy: String(requestedBy || '').trim(),
status: 'open',
createdAt: new Date().toISOString()
});
}
async function setRequestStatus(id, status) {
const allowed = ['open', 'ordered', 'done'];
if (!allowed.includes(status)) return;
const all = (await window.NorthernGoDB.get('requests')) || [];
const row = all.find(item => item.id === id);
if (!row) return;
row.status = status;
await window.NorthernGoDB.save('requests', row);
}
Closed status lists again. If the model is allowed to invent "pending review", you will have four names for waiting and a dashboard that undercounts.
E-mail is optional and easy to overuse. window.NorthernGo.sendEmail(to, subject, html) can notify someone that a request arrived. It is not a ticket system, it does not thread replies, and the recipient still has to open the app to change the status. Use it for a ping, not as the database.
Who is "requestedBy"? The honest answer is the signed-in e-mail, not a free-text name field the model adds because it looks friendly. A name field is how you get "Micke" and "Mikael" as two people.
Step 4: A dashboard that reads the same collections
Do not store totals. Compute them when the overview renders.
async function loadOverview() {
const items = (await window.NorthernGoDB.get('items')) || [];
const requests = (await window.NorthernGoDB.get('requests')) || [];
const low = items.filter(row => Number(row.quantity) <= Number(row.minQuantity)).length;
const open = requests.filter(row => row.status === 'open').length;
const newest = items
.map(row => row.lastTouchedAt)
.filter(Boolean)
.sort()
.at(-1) || '—';
renderOverview({ low, open, newest, lowItems: items.filter(row => Number(row.quantity) <= Number(row.minQuantity)) });
}
Three numbers and the actual low-stock rows underneath. That is a dashboard a workshop will look at. A chart library with no new information is what the model adds when you say "make it insightful". Strike that sentence from the prompt.
Refresh on load and after every save. A dashboard that caches in a variable and never re-reads is how the office and the yard disagree.
Step 5: Auth is not optional on internal data
This step is the one people skip because "it is only us".
window.addEventListener('DOMContentLoaded', async () => {
if (!window.NorthernGoDB.getToken()) {
showLogin();
return;
}
await loadOverview();
});
async function handleLogin(email, password) {
try {
await window.NorthernGoDB.login(email, password);
location.reload();
} catch (err) {
showMessage(err.message);
}
}
Create staff accounts with register yourself, then stop showing the register form in the published app if you can help it. An open register button on an internal tool is a way for a stranger to create an empty account. They will not see your rows — records are scoped to the signed-in user — but they will think they have access, and you will think the tool is broken when they phone you.
Shared data among staff has the same constraint as a CRM. Two logins do not see one inventory. A workshop that needs one list uses one shared login, or accepts that this is a personal checklist. There are no roles. An "Admin" toggle the model draws does not protect a delete button.
Logout belongs next to the title, not buried. window.NorthernGoDB.logout() then reload. Shared phones in a break room are the reason.
Delete-account stays, even here:
if (confirm('This permanently deletes your account and all your data. Continue?')) {
await window.NorthernGoDB.deleteAccount();
location.reload();
}
You are the controller for whatever the tool stores. NorthernGo is the processor for the default EU database. Staff asking to be removed is not a weird request; it is the law.
Step 6: Publish it like an internal URL, not like a product
Publish from My projects. Free always lands on the fallback address. Premium and Pro get a northerngo.com subdomain; Pro can bind a custom domain. For a workshop tool the subdomain is enough. A custom domain is for when the URL has to look like you, not like the builder.
Treat the link as a secret that is not actually secret. Anyone with it still hits the login screen if you gated correctly. If you did not gate, anyone with it is in the stock list. Test that from a private window before you paste the URL into a chat.
ZIP export on Premium and Pro is the exit if the tool becomes important. Vanilla JS and Tailwind, one file, no build. Host it yourself later. That is the practical answer to vendor lock-in for internal software: you can leave with the screens and, via get(), with a JSON dump of the collections.
What this will not become
- Not an ERP or a BI tool. A quantity field, a location string, three counts from
get(). No purchase orders, no warehouse connector. - Not a permission matrix. E-mail and password, per-user collections. Shared inventory means a shared login.
- Not a conflict resolver across devices. Offline counts queue and last-known numbers show; two devices can still diverge until both are online.
- Not a replacement for Fortnox. Pro has webhooks and Fortnox for a later pipe. The first version should close one paper form.
What it costs
Free: 0 SEK, five cloud generations a month, unlimited local. No ZIP. Premium: 179 SEK / $19, 25 cloud, ZIP. Pro: 299 SEK / $29, 50 cloud, custom domains, Shopify, SEO audits, webhooks and Fortnox. Buy Pro for the domain or a webhook, not because a request form needs a shop.
Troubleshooting
The overview is always zero. get() failed or returned a non-array. Use || [] and coerce with Number. Two rows for the same SKU means no match-before-insert.
A staff member sees empty shelves. Per-user storage. Share one login.
Someone opened the URL and saw last week's stock. The lists rendered before the token check. Gate at the top of DOMContentLoaded. Hide register on the published tool.
Frequently asked questions
Is an AI app builder actually faster for internal tools than buying SaaS?
For one inventory list, one request form and a three-number dashboard, yes — you can describe the fields in ordinary language and have a working screen the same day. For purchase orders, permissions and accounting, no. The builder wins when the job is small and specific. It loses when you need the product someone else already maintains.
Do internal tools built with AI still need login?
Yes. Without a session, anyone with the published URL reads and writes the same collections. Internal does not mean hidden. Put window.NorthernGoDB.login in front of every list and save, and keep a delete-account button because staff still have GDPR article 17 rights.
Can two people in a workshop share one inventory list?
Not as two separate accounts. Records belong to the signed-in user and there is no role or invite API. Use one shared login, or treat the list as personal. An Admin badge the model draws is not access control.
Is the free plan enough to build an internal dashboard?
Enough to see whether the three screens appear: five cloud generations a month plus unlimited local WebGPU or Ollama. Not enough if you need ZIP export. Premium at 179 SEK / $19 adds 25 cloud generations and the ZIP. The database and login are included on Free.
Will the dashboard work in a warehouse with bad signal?
The PWA shell loads without a network. get() shows the last synced numbers plus queued saves; save/delete replay when the app is online again. Login and in-app AI still need a connection. Two devices can diverge until both sync.
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.