AI Features & Travel · 8 min read · Updated 15 August 2026
How to build an AI travel assistant that recommends real places
A travel concierge app built in NorthernGo is two parts: a collection of places you store with `window.NorthernGoDB.save()`, and one `window.NorthernGoAI.generate()` call that turns a traveller’s stated preferences into a ranked selection from that collection. The model does not know your town, so grounding every prompt in your own records is what makes the recommendations real. In-app AI requires Premium or Pro.
What a travel concierge app actually is
Strip away the framing and an AI travel assistant is a small application with three moving parts: a store of places, a form where somebody says what they are in the mood for, and one language model call that picks from the store and explains why. That is the whole shape of it.
The interesting decision is not which model to use. It is deciding what the model is allowed to invent and what it must read from your data. Get that boundary wrong and you ship a confident liar: ask a model for "hidden restaurants in Lycksele" and it will return plausible names, plausible opening hours and plausible street addresses, none of which have to exist. The traveller drives there and finds a car park.
Get the boundary right and you have something genuinely worth publishing — your own curated local knowledge, retrieved and phrased by a model that is very good at phrasing and very bad at remembering small towns. This guide builds the second version.
The place records come first, not the AI
Before writing a single prompt, decide what a place looks like as a record. Every field you leave out is a field the model will be tempted to fill in for you.
A workable shape covers the things people filter on and the one thing they actually want:
const places = [
{
id: crypto.randomUUID(),
name: 'Ansia',
area: 'Lycksele',
category: 'outdoors',
season: 'summer',
priceLevel: 1,
openingNote: 'Unstaffed. Quietest before 10:00.',
why: 'River swimming spot the campsite guests walk past without stopping.'
}
];
The why field is the one that matters. It is the sentence only a local could write, and it is the reason the finished app is better than a general-purpose chatbot. The model's job is to select and rephrase it, never to originate it.
Step 1: Load the places into the database
Every app generated in NorthernGo already has a Supabase PostgreSQL database attached through window.NorthernGoDB, with no connection string to paste and no table to define. Writing your seed data is a loop:
async function seedPlaces() {
for (const place of places) {
try {
await window.NorthernGoDB.save('places', place);
} catch (err) {
console.error('Could not save', place.name, err);
}
}
}
Run that once. After that the collection exists and get('places') returns it. The full database API is four methods wide — save, get, delete and the auth calls — which is enough for this app and worth reading before you extend the data model.
For a real deployment, put the seeding behind an admin screen rather than shipping it as a loop that runs on load. A loop that runs on every page load will duplicate your entire place list on every visit.
Step 2: Ask the model to choose, not to remember
Now the AI call. The signature is generate(prompt, isJson, base64Img). Passing true as the second argument makes it parse the reply as JSON and hand you an object, which is what you want here — you are asking for a selection, not for prose.
async function recommend(preferences) {
const all = (await window.NorthernGoDB.get('places')) || [];
const prompt = [
'You are a local guide. Choose at most three places from the JSON list below.',
'Use ONLY entries from the list. Never invent a place, an address or an opening time.',
'If nothing in the list fits, return an empty picks array.',
'Reply as JSON: { "picks": [{ "id": "...", "pitch": "..." }] }',
'',
'The traveller wants: ' + preferences,
'Available places: ' + JSON.stringify(all)
].join('\n');
return await window.NorthernGoAI.generate(prompt, true);
}
Three things in that prompt are doing real work. Asking for id rather than name means you can look the record up rather than trust the text. Telling it to return an empty array when nothing fits gives it a legitimate way to say no, which is the only reliable alternative to making something up. And putting the list in the prompt means the recommendation is bounded by your data rather than by the model's training.
Step 3: Verify the reply before you render it
This is the step most tutorials skip, and it is the difference between a demo and an app. The SDK's generate() catches its own errors and resolves to null — it does not throw. If you forget to check for that, a failed call renders as a blank panel with no explanation.
On top of that, a model told not to invent things will still occasionally invent things. So check the ids against what you actually have:
async function showRecommendations(preferences) {
const all = (await window.NorthernGoDB.get('places')) || [];
const result = await recommend(preferences);
if (!result || !Array.isArray(result.picks)) {
showMessage('Could not reach the guide right now. Showing everything instead.');
renderPlaces(all);
return;
}
const byId = new Map(all.map(p => [p.id, p]));
const verified = result.picks
.filter(pick => byId.has(pick.id))
.map(pick => ({ ...byId.get(pick.id), pitch: pick.pitch }));
if (verified.length === 0) {
showMessage('Nothing in the guide matches that yet.');
renderPlaces(all);
return;
}
renderPlaces(verified);
}
Note what the fallback does: it degrades to your own unfiltered list rather than to an error screen. A travel app that shows all forty places when the AI is unavailable is still a useful travel app.
Step 4: Let travellers keep what they picked
Without login, every visitor writes to the same collection and sees everyone else's saved trips. Adding accounts scopes records to the signed-in person:
await window.NorthernGoDB.register(email, password);
await window.NorthernGoDB.login(email, password);
if (window.NorthernGoDB.getToken()) {
await window.NorthernGoDB.save('itineraries', {
id: crypto.randomUUID(),
day: '2026-07-14',
placeIds: verified.map(p => p.id)
});
}
Store the ids, not copies of the place records. When you correct an opening time next spring, every saved itinerary picks up the correction instead of preserving last year's mistake.
An app with login also needs a visible way to delete the account — that is GDPR article 17 rather than a nice-to-have, and window.NorthernGoDB.deleteAccount() handles the auth record and the data together.
Step 5: Publish it somewhere a traveller can reach
Publish from My projects. Where the app ends up depends on the plan: a Pro custom domain if you have bound one, otherwise a <subdomain>.northerngo.com address on Premium and Pro, otherwise the fallback https://northerngo.com/?app=<projectId>. Free plan projects always land on the fallback.
For travel specifically, the subdomain route matters more than it looks. Published apps on a subdomain or custom domain get a service worker, so the app shell still loads on a phone with no signal. get() can show the last synced trips plus queued saves; login and in-app AI still need a connection. Stale opening hours can appear until the next successful sync — say so in the UI. The offline-first patterns are worth applying here.
What it costs
Two separate budgets, and confusing them is the usual billing surprise.
Building the app in the editor spends cloud generations: five per month on Free, 25 on Premium at 179 SEK / $19, 50 on Pro at 299 SEK / $29. Generating locally on your own GPU is unlimited on every plan.
Travellers using the finished app spend a different budget. The in-app AI proxy is capped per project per day — 500 text calls, 100 generated images and 300 speech calls — with a short-term rate limit of 15 requests a minute. Text calls run on Gemini 3.1 Flash Lite. For a guide with a few hundred visitors a day this is comfortable; for something at real scale it is a ceiling you should know about before launch day.
Limitations
- No maps, no geocoding, no location. There is no built-in map component and no place lookup service. Travellers tell the app where they are by typing or picking from your list of areas. If you need a real map you are adding a third-party library to exported code, not switching on a feature.
- In-app AI is Premium or Pro only. On Free the AI agents are explicitly instructed not to build
window.NorthernGoAIcalls into your app, and the proxy rejects them at runtime if you add them by hand. Free is for building and shipping the non-AI parts. - The model has no memory between calls. Each
generate()is independent. Conversational follow-up ("something cheaper than that") only works if you resend the earlier turns as part of the new prompt. - No live availability. Nothing here checks whether a restaurant is open now or a room is free tonight. It recommends from your records, which are as current as you last made them.
- Recommendations are only as good as the
whyfields. Twenty places with thin one-line notes produce twenty thin recommendations. This is a curation project with an AI layer, not the reverse. Builders promising a finished travel product from a single sentence are selling you something different.
Troubleshooting
The AI panel is blank and the console is clean. generate() returned null. It swallows its own errors, so add the explicit if (!result) branch from step 3 and log inside it. On Free, null is the expected result — the proxy refused the call.
It recommends places that are not in my database. The reply was rendered without verification. Ask for ids, look each one up, drop anything unrecognised. Prompt wording alone will not fix this.
Recommendations ignore the preferences. Usually the place list is too large for the model to reason over. Filter in JavaScript first — by area, season or category — and send the shortlist rather than the whole collection.
Saved trips appear for the wrong traveller. That is the expected behaviour without login. Records written with no session are stored at app level and shared by everyone.
It works in the editor but not on the published app. Check which URL you are testing. A Free project always redirects to the ?app= fallback, and the in-app AI stays blocked there regardless of where the app is served from.
Frequently asked questions
Can an AI app builder make a travel app that actually knows my town?
Only if you supply the knowledge. The language model has no reliable information about small places and will invent addresses and opening hours if asked to recall them. The working pattern is to store your own place records in the app database and send that list with every prompt, so the model selects and phrases rather than remembers.
Why does my AI travel app return nothing at all on the free plan?
In-app AI features are blocked on Free. The AI proxy rejects the request because the project owner is on the free plan, and because window.NorthernGoAI.generate catches its own errors and resolves to null, the app shows an empty result instead of an error. Premium or Pro is required for AI inside a published app.
How do I stop the AI making up restaurants and opening hours?
Instruct it to pick only from a supplied list, ask it to return record ids rather than names, and then verify every id against your database before rendering. Prompt wording reduces invention but does not eliminate it; the id check is what makes it impossible for an invented place to reach the screen.
Does the travel app work offline when I am abroad without data?
Partly. An app published to a NorthernGo subdomain or a custom domain gets a service worker, so the interface still loads without a connection. get() shows last-synced trips plus queued saves; login and in-app AI still need a connection.
Can I add a map with the traveller’s current position?
Not as a built-in feature. There is no map component and no geocoding or location service in the platform, so travellers indicate where they are by typing or choosing from the areas you defined. A real map means exporting the source code and adding a mapping library yourself, which is possible on Premium and Pro through ZIP export.
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.