Aamu.app's Database and GraphQL API can be used as the data layer for a small web application. In this example, we will build a simple reservation calendar: the page lists existing reservations from an Aamu Database and lets visitors add a new reservation.
The important update is security. A public single-page app should not contain a database API key in its JavaScript. Use the GraphQL API from a server-side endpoint, serverless function, or backend proxy. For public form submissions, use the Aamu Forms endpoint, which is designed for browser-side submissions and does not expose a general database API key.
What we are building
The application has two jobs:
read existing rows from an Aamu Database with GraphQL, and
add new rows through an Aamu Forms endpoint.
The safe production flow looks like this:
Browser SPA
-> your /api/reservations endpoint
-> Aamu GraphQL API
-> Aamu Database
Browser form
-> Aamu Forms endpoint
-> Aamu DatabaseThe browser talks to your own public endpoint for reading data. Your endpoint keeps the Aamu API key private and forwards the GraphQL request to Aamu. For adding a row, the browser can post to the Forms endpoint because that endpoint is intentionally limited to form submissions.
Why split reads and writes?
GraphQL is the flexible database API. It can read rows and, depending on permissions, write or update data. That flexibility is useful, but it also means the API key should be treated as a server-side secret.
The Forms endpoint is narrower. It is meant to accept public submissions into a configured table. That makes it a better fit for browser-side "add this form submission" actions.
So the practical rule is:
Use GraphQL from server-side code.
Use Forms endpoint from public HTML or frontend JavaScript.
Do not publish an Aamu database API key in browser code.
Create the database
Start by creating a database in Aamu.app. For this example, imagine a simple reservation table with fields like:
TitleStart time
The exact table and field names affect the generated GraphQL type and field names. In the old example, the table was called Sheet1, which produced a collection named Sheet1Collection. In a real app, give the table a clearer name, such as Reservation.
Enable Forms for new reservations
Open the database settings and enable Forms. Select the reservation table as the destination table. Copy the Forms endpoint.
Your form can then submit new rows without needing an API key in the browser:
<form action="FORMS_ENDPOINT_HERE" method="POST">
<label>
Title
<input name="title" required>
</label>
<label>
Start time
<input name="start_time" type="datetime-local" required>
</label>
<button type="submit">Reserve</button>
</form>Use the field names shown in the Aamu Forms settings. They are the source of truth for the input name attributes.
Read rows with a backend endpoint
For reading existing reservations, create a small server-side endpoint. It can be a Node server, a serverless function, a Cloudflare Worker, a Vercel function, a Netlify function, or any backend you already use.
The backend stores these values privately:
AAMU_API_KEY=your_private_api_key
AAMU_DB_ID=your_database_id
AAMU_GRAPHQL_ENDPOINT=https://api.aamu.app/api/v1/graphql/Then it sends the GraphQL request to Aamu:
export async function listReservations() {
const response = await fetch(process.env.AAMU_GRAPHQL_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'x-api-key': process.env.AAMU_API_KEY,
'x-db-id': process.env.AAMU_DB_ID
},
body: JSON.stringify({
query: [
'{',
' ReservationCollection(sort: { startTime: ASC }) {',
' id',
' title',
' startTime',
' }',
'}'
].join('\n')
})
});
if (!response.ok) {
throw new Error('Aamu GraphQL error: ' + response.status);
}
const data = await response.json();
if (data.errors?.length) {
throw new Error(data.errors.map((error) => error.message).join('; '));
}
return data.data.ReservationCollection;
}Adjust the collection name and fields to match your database schema. If your table is still named Sheet1, the collection may be Sheet1Collection. If your table is named Reservation, the generated collection name is easier to understand.
Call your backend from the SPA
Now the browser can fetch reservations from your own endpoint:
async function getReservations() {
const response = await fetch('/api/reservations');
if (!response.ok) {
throw new Error('Failed to load reservations: ' + response.status);
}
const reservations = await response.json();
renderReservations(reservations);
}
getReservations();The frontend never sees the Aamu API key. It only sees the data your endpoint returns.
Submit new rows with Forms
For a smoother user experience, the SPA can submit the form with JavaScript while still using the Forms endpoint:
const form = document.querySelector('form');
form.addEventListener('submit', async (event) => {
event.preventDefault();
const response = await fetch(form.action, {
method: 'POST',
body: new FormData(form)
});
if (!response.ok) {
throw new Error('Reservation failed: ' + response.status);
}
form.reset();
await getReservations();
});This keeps writes simple. The Forms endpoint accepts the submission, Aamu stores a new row, and the app reloads the visible list from your backend endpoint.
What about a pure HTML demo?
For a local demo or learning exercise, you may see examples where the API key is placed directly in the HTML file. That can be useful to understand the moving parts, but it is not appropriate for a public website.
Once the page is public, everything in the HTML and JavaScript is public too. If a database API key is there, visitors can read it. Treat that as a hard line: use a backend for GraphQL.
What this gives you
This small architecture is already useful:
Aamu Database stores the structured data.
GraphQL gives the app flexible read access from server-side code.
Forms endpoint lets the browser add rows safely.
The frontend stays a normal single-page app.
The team can still open Aamu and work with the rows directly.
That last point is the quiet advantage. You are not only building an app around a database. You are building an app around a database that your team can also use inside the same workspace as docs, tasks, automations, and customer work.
Testing checklist
If the app does not work, check these first:
The backend has
AAMU_API_KEYandAAMU_DB_IDset.The GraphQL query uses the generated collection and field names for your database.
The browser calls your backend endpoint, not Aamu GraphQL directly.
The form action points to the current Forms endpoint.
The input
nameattributes match the Forms field bindings.After submitting, a new row appears in the Aamu Database.
That's it
The safe version of this app is still small: a single-page frontend, a tiny backend endpoint for GraphQL reads, and a Forms endpoint for browser-side submissions. That is enough to build many useful public-facing tools without exposing your database API key.
