Skip to main content

Connect to external databases

Flexweg hosts static files — HTML, CSS, JavaScript, images. There is no server-side runtime, no PHP, no Node, no database running on your behalf. But "static" doesn't mean "no dynamic data": your pages can read from and write to external services from the browser or at build time.

This page lists the common patterns and the services that implement them, with runnable code examples.

How a static site handles data

Three moments where data can enter your site:

  1. Build time — your CI job queries an API or database, bakes the results into the generated HTML, and deploys. Ideal for data that changes infrequently (product catalogs, blog post lists, pricing).
  2. Runtime, from the browser — JavaScript on the page calls an API directly. Ideal for per-visitor data (search, user account, live feeds).
  3. Runtime, via a proxy — the browser calls a small serverless function, which in turn talks to the database with secret credentials. Required whenever you need to hide an API key, compute something server-side, or write to a protected resource.

The rest of this page walks through the services and patterns that implement each of these.

Pattern 1 — Call a public API from the browser

The simplest case: your JavaScript fires a fetch() against a public, CORS-enabled API. No backend, no secrets, no build magic.

index.html
<!DOCTYPE html>
<html>
<head><title>GitHub profile card</title></head>
<body>
<div id="profile">Loading…</div>

<script>
fetch('https://api.github.com/users/torvalds')
.then((r) => r.json())
.then((data) => {
document.getElementById('profile').innerHTML = `
<img src="${data.avatar_url}" width="80">
<h1>${data.name}</h1>
<p>${data.bio}</p>
<p>${data.public_repos} public repos</p>
`;
});
</script>
</body>
</html>
No secrets in client JS

Anything shipped to the browser is visible in the page source. Never paste an API key that has write or admin privileges — anyone can copy it and use your quota or wipe your data. Public read-only APIs (GitHub public, OpenWeatherMap free tier, etc.) are fine; everything else needs a proxy (see Pattern 5).

Pattern 2 — Fetch at build time and bake the data

If your data doesn't change minute-to-minute, pull it once during the GitHub Actions build and write the result into a JSON file (or directly into generated HTML). The deployed site stays 100% static.

Example: a GitHub Actions step that fetches product data before the site is built:

.github/workflows/deploy.yml
- name: Fetch data
env:
SHOP_API_KEY: ${{ secrets.SHOP_API_KEY }}
run: |
curl -H "Authorization: Bearer $SHOP_API_KEY" \
https://api.myshop.com/v1/products > data/products.json

- name: Build site
run: npm run build

- name: Deploy to Flexweg
env:
FLEXWEG_API_KEY: ${{ secrets.FLEXWEG_API_KEY }}
run: node sync-flexweg.js

Your static site generator (Docusaurus, 11ty, Astro, Hugo, Next.js export…) reads data/products.json and renders pages from it. Re-run the workflow on a cron if you want the data to stay fresh.

Refresh on a schedule

Add a cron trigger to the workflow and it rebuilds automatically — say, every hour:

on:
push: { branches: [main] }
schedule:
- cron: '0 * * * *' # top of every hour

Pattern 3 — Use a Backend-as-a-Service (BaaS)

The sweet spot for most apps: a hosted service that gives you a database, auth, file storage and a REST or JS SDK — all with a free tier. Your static site reads and writes directly from the browser.

Supabase (Postgres + Auth + Realtime)

Supabase is an open-source Firebase alternative built on PostgreSQL. Free tier includes 500 MB DB + 50,000 monthly active users.

index.html
<script type="module">
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';

const supabase = createClient(
'https://YOUR-PROJECT.supabase.co',
'YOUR_ANON_PUBLIC_KEY' // safe to ship — row-level security protects the data
);

// Read
const { data: posts } = await supabase.from('posts').select('*').limit(10);
console.log(posts);

// Write (if the user is authenticated and RLS allows it)
await supabase.from('comments').insert({ post_id: 1, body: 'Nice article!' });
</script>

Why it's safe to ship the anon key: Supabase uses Row Level Security (RLS) policies enforced server-side. The anon key only lets the client do what your RLS policies explicitly allow.

Firebase (NoSQL + Auth + Hosting)

Firebase is Google's BaaS. Firestore is a real-time NoSQL database with a generous free tier.

index.html
<script type="module">
import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.13.0/firebase-app.js';
import { getFirestore, collection, getDocs } from 'https://www.gstatic.com/firebasejs/10.13.0/firebase-firestore.js';

const app = initializeApp({
apiKey: 'YOUR_PUBLIC_API_KEY',
authDomain: 'your-app.firebaseapp.com',
projectId: 'your-app',
});

const db = getFirestore(app);
const snap = await getDocs(collection(db, 'posts'));
snap.forEach((doc) => console.log(doc.data()));
</script>

Like Supabase, the API key is safe to ship — Security Rules enforce what each caller is allowed to do.

PocketBase (self-hosted, single binary)

PocketBase is a single executable providing SQLite DB + auth + file storage + admin UI. Run it on a $5 VPS and you have a full backend. Free, no SaaS lock-in.

index.html
<script type="module">
import PocketBase from 'https://esm.sh/[email protected]';

const pb = new PocketBase('https://your-pocketbase.example.com');

const posts = await pb.collection('posts').getList(1, 20);
console.log(posts.items);

// Auth
await pb.collection('users').authWithPassword('[email protected]', 'password');
await pb.collection('comments').create({ post: 'abc123', body: 'Hi' });
</script>

Airtable (spreadsheet as database)

Airtable lets non-technical teammates edit your data in a familiar spreadsheet UI. Queryable via REST.

const res = await fetch(
'https://api.airtable.com/v0/appXXXX/Products',
{ headers: { Authorization: 'Bearer YOUR_AIRTABLE_PAT' } }
);
const { records } = await res.json();
Airtable tokens are sensitive

Unlike Supabase/Firebase anon keys, Airtable Personal Access Tokens grant full access to the base. Never ship them to the browser — use the proxy pattern instead, or fetch at build time.

Hasura (GraphQL on top of Postgres)

Hasura auto-generates a GraphQL API over any Postgres database. Pair it with Neon or Supabase for a free Postgres and you get queryable, typed, real-time data.

const query = `
query { products(limit: 10) { id name price } }
`;
const res = await fetch('https://your-hasura.example.com/v1/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query }),
});
const { data } = await res.json();

Pattern 4 — Google Sheets as a simple database

Need something a non-technical teammate can edit? Publish a Google Sheet and read it as JSON — no developer account, no billing, no SDK.

  1. In your sheet: File → Share → Publish to web → Entire document, CSV.
  2. Copy the published URL.
  3. From the browser:
const res = await fetch('https://docs.google.com/spreadsheets/d/SHEET_ID/export?format=csv');
const csv = await res.text();
const [headers, ...rows] = csv.trim().split('\n').map((r) => r.split(','));
console.log(headers, rows);

Great for pricing tables, event schedules, a small catalog. For writes, use Google Apps Script to expose a web endpoint that accepts POST requests and writes to the sheet.

Pattern 5 — Hide secrets with a serverless proxy

When you need to call an API that requires a secret key with real privileges (write access, admin scope, paid-tier quota), never expose the key in the browser. Put a small serverless function in front.

Cloudflare Workers (most generous free tier)

100,000 requests/day free, no credit card required. Works as a proxy or as a full tiny backend.

worker.js
export default {
async fetch(request, env) {
// CORS pre-flight
if (request.method === 'OPTIONS') {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': 'https://your-site.flexweg.com',
'Access-Control-Allow-Methods': 'GET, POST',
'Access-Control-Allow-Headers': 'Content-Type',
},
});
}

// Proxy to the real API with the secret key from the Worker's environment
const res = await fetch('https://api.myshop.com/v1/products', {
headers: { Authorization: `Bearer ${env.SHOP_API_KEY}` },
});
const data = await res.json();

return new Response(JSON.stringify(data), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': 'https://your-site.flexweg.com',
},
});
},
};

Deploy it at https://your-proxy.workers.dev, then from your Flexweg site:

const res = await fetch('https://your-proxy.workers.dev');
const data = await res.json();

The SHOP_API_KEY lives in the Worker's encrypted environment — never shipped to browsers.

Other serverless options

All follow the same pattern: a small function with an environment-scoped secret, called from your Flexweg site over HTTPS.

Collecting form submissions (built-in)

Before you reach for a full BaaS, check if you just need form submissions. Flexweg has a built-in forms system:

  • Create a form via the dashboard or the Forms API
  • Drop an HTML or React snippet into your Flexweg site
  • Receive submissions with spam filtering, listable and filterable via API

No external database required — see Forms API for endpoints and MCP tools for AI-driven form management.

Security essentials

A static site that talks to external services has a smaller attack surface than a traditional app, but a few rules still apply:

  1. Never ship secret keys to the browser. If a key lets you write, delete or pay, it must live in a serverless proxy or build-time environment — never in the HTML/JS.
  2. Use the provider's security rules. Supabase RLS, Firebase Security Rules, Hasura permissions — all are designed to let you safely expose a public client key. Turn them on and write real rules.
  3. Lock down CORS. Your API (or proxy) should only accept requests from your Flexweg domain. Return the exact origin in Access-Control-Allow-Origin, not *, for anything authenticated.
  4. Rate-limit. Serverless proxies can enforce per-IP limits cheaply. Your BaaS provider also has dashboard alerts for abnormal traffic.
  5. Treat API keys like passwords. Rotate them if compromised. Store them in GitHub secrets, provider env vars, or Cloudflare Worker secrets — never in the repo.

Which pattern should I pick?

NeedBest pattern
Display public data (GitHub, weather, maps)Call a public API
Product catalog, changes dailyFetch at build time
Blog comments, likes, user profilesSupabase or Firebase
Real-time chat or live feedSupabase Realtime or Firebase Firestore
Small app with zero SaaS lock-inPocketBase on a VPS
Non-technical teammates edit the dataAirtable or Google Sheets
Need to hide a secret keyServerless proxy
Collect form submissionsFlexweg Forms

Next steps

  • Browse the Forms API if form submissions cover your need.
  • Check the MCP tools for forms to let AI assistants create and manage forms for you.
  • Read the build-time section of the Docusaurus use case to see where in your CI workflow a data-fetching step belongs.