Vercel is the industry-standard platform for frontend frameworks (Next.js). It handles hosting, edge caching, and serverless functions. Baserow acts as the structured backend, managing the content, inventory, or user data that powers the frontend.
In an enterprise environment, separating the frontend (Vercel) from the data layer (Baserow) allows for a Headless Architecture. This gives you the best of both worlds:
We will connect Vercel to Baserow using the REST API to create a universal bridge.
GET): Vercel fetches listings from Baserow to render the page (e.g., Job Board, Blog, Directory).POST): When a user submits a form, Vercel Serverless Functions securely sanitize and forward the data to Baserow.Read permissions (for content) and Create permissions (for submissions).BASEROW_API_TOKEN: Your raw API key.BASEROW_API_URL: https://api.baserow.io (or your self-hosted URL).TABLE_ID: The ID of the specific table you are accessing.| Area | In Vercel (The Frontend) | In Baserow (The Backend) |
|---|---|---|
| Recruitment (ATS) | Displays the Careers page. Renders job descriptions and application forms dynamically using Server-Side Rendering (SSR) or Static Site Generation (SSG). | Stores job titles, descriptions, status (Open/Closed), and receives incoming applicant data (Name, Resume link). |
| Product Inventory | A fast e-commerce storefront. Fetches product details and stock status at build time for maximum SEO performance. | Manages SKUs, pricing, stock levels, and product images. |
| Programmatic SEO | Generates thousands of landing pages (e.g., “Best Lawyers in [City]”) based on URL patterns. | Stores the structured datasets (Cities, Professions, Metadata) that populate these pages. |
| Internal Tools | A custom employee dashboard for expense reporting or leave requests protected by Vercel’s edge authentication. | Acts as the system of record for approved expenses and employee logs. |
We will show you how to pipe map data from Baserow into your UI. These universal steps apply whether you are building a blog, a job board, or a directory.
Start by building the data structure.
Never hardcode your API keys in your frontend code. Vercel handles secrets securely.
This allows Vercel to fetch data from Baserow during the build.
BASEROW_API_TOKEN: Your generated database token (e.g., eyJ...). Just the raw key, without "Token " prefix, so it matches the code below.BASEROW_API_URL: https://api.baserow.io (or your self-hosted URL).POSITIONS_TABLE_ID: The ID of your Positions table.APPLICANTS_TABLE_ID: The ID of your Applicants table.Click Save. A redeploy is required for these to take effect.
Now we build the bridge. Instead of writing fetch requests inside every single page component (which is messy and hard to maintain), we will add a reusable function that connects your Next.js app to Baserow.
Create (or update) a file in your library folder where data fetching happens (e.g. lib/api.ts or lib/api.js).
In this case, we filter specifically for jobs where Is filled is false, and extract the results array from the Baserow response so the frontend receives a clean list.
Add the following code to the bottom of the file. This allows you to keep your existing template logic while adding Baserow capabilities:
// --- BASEROW CONNECTORS ---
// 1. SECURITY: Centralizes authentication so we don't repeat the API Token everywhere.
const getBaserowHeaders = () => {
return {
Authorization: `Token ${process.env.BASEROW_API_TOKEN}`,
};
};
// 2. LIST FETCHER: Gets all rows from any table you specify.
export async function getBaserowData(tableId: string) {
const baseUrl = process.env.BASEROW_API_URL;
// Baserow API filter for Boolean fields where 'Is filled' is FALSE
const query = new URLSearchParams({
user_field_names: 'true',
filters: '{"filter_type":"AND","filters":[{"type":"boolean","field":"Is filled","value":"0"}]}'
});
// We add 'user_field_names=true' so we get "Title" instead of "field_123"
const res = await fetch(
`${baseUrl}/api/database/rows/table/${tableId}/?${query}`,
{
headers: getBaserowHeaders(),
next: { revalidate: 60 }, // ISR: Caches data but auto-updates every 60 seconds
}
);
if (!res.ok) {
throw new Error(`Failed to fetch data from table ${tableId}`);
}
const data = await res.json();
// OPTIONAL ROUTING: Maps the internal Baserow Row ID to a 'slug' for Next.js URLs.
return data.results.map((item: any) => ({
...item,
slug: item.id.toString(),
}));
}
// Helper to get a Single Row (for Detail Pages)
export async function getBaserowRecord(recordId: string, tableId: string) {
const baseUrl = process.env.BASEROW_API_URL;
const res = await fetch(
`${baseUrl}/api/database/rows/table/${tableId}/${recordId}/?user_field_names=true`,
{
headers: getBaserowHeaders(),
next: { revalidate: 60 },
}
);
if (!res.ok) {
return null;
}
return res.json();
}
In Next.js 15+, ensure you await your params before passing them to the fetcher.
It handles the authentication headers and sets up the caching strategy automatically: one to get the list and one to get a single row.
Now open the page where you want to display your data (e.g., app/page.tsx).
We are going to perform a data swap. We will locate the existing data in your template and replace it with the live function we just created.
The concept is to find the variable holding your data and replace it with an await call to your new connector.
A. Import the Connector
import { getBaserowData } from "@/lib/api"; // ✅ Import the universal connector
B. Swap the Data Source
In your main component, find where the data is defined. It may look like a static array const items = [...] or a local fetch.
Replace that line with an asynchronous call to your new function to fetch live data from Baserow during the build.
export default async function Index() {
// ✅ Use the universal function with your specific Table ID
const jobs = await getBaserowData(process.env.POSITIONS_TABLE_ID!);
return (
// ... your existing JSX ...
)
}
C. Mapping Your Fields (Update Loop)
This is the most common stumbling block when moving from a JSON file to a database. When your template iterates over the data, you need to ensure the field names match your Baserow columns exactly.
Object Handling:
In a simple JSON file, Department might just be the text “Engineering”.
Baserow is a relational database, not a flat CSV. Complex field types like Single Select (e.g., Department) or Files are returned as objects, not strings, so they can carry extra metadata like color tags.
Baserow API response for Department Single Select field:
"Department": {
"id": 55,
"value": "Engineering",
"color": "blue"
}
If you try to render an object directly in your JSX (e.g., {job.Department}), React will throw an error because it cannot render an object. You must point to the specific .value property.
Case Sensitivity: If your Baserow field is named “Title”, you must use
job.Titlein your code.job.titlewill return undefined.
Conditional Rendering
Here is how to safely handle this inside your map loop. We check if the data is an object (from Baserow) or just text (from your old data), so your app doesn’t break during the transition.
Update your app/page.tsx:
{jobs.map((job: any) => {
// Baserow "Single Select" fields return an object: { id: 1, value: "Engineering", color: "blue" }
// Standard text fields just return a string: "Engineering"
// We use this check to handle both cases safely.
const departmentName =
typeof job.Department === "object" && job.Department !== null
? job.Department.value
: job.Department;
return (
<div key={job.id}>
<h2>{job.Title}</h2>
{/* Render the clean value, not the raw object */}
<span className="tag">{departmentName}</span>
{/* Use the Row ID for the link */}
<Link href={`/posts/${job.id}`}>View Details</Link>
</div>
);
})}
Now that your application can Read the open positions, we want to build the input form to Write the candidate’s application back to Baserow.
Reading data is safe to do publicly, but writing data requires strict security. You cannot place your BASEROW_API_TOKEN inside a frontend button or form component. If you do, any user can right-click “Inspect Element” and steal your database access.
To solve this, we use a Server-Side Proxy. Your frontend sends data to your own server (Vercel), and your server, which holds the secret keys, forwards it to Baserow.
In Next.js, we don’t need to spin up a separate backend server for this. We can use a Server Action. This allows you to write a function that looks like frontend code but runs securely on the server.
A: The Universal Write Connector
You can drop this logic into any page where you need to save data (e.g. src/app/posts/[slug]/page.tsx).
Locate the component where your form lives (e.g., your Contact page or Signup modal). Inside that component, paste this “Server Action” function.
The Infrastructure Code:
// ---------------------------------------------------------
// THE SERVER ACTION (The Secure Bridge)
// ---------------------------------------------------------
async function submitToBaserow(formData: FormData) {
'use server'; // <--- Critical: This ensures the code runs on Vercel, not the browser
// 1. Capture data from your form
const formField1 = formData.get("myFieldName");
const formField2 = formData.get("myOtherFieldName");
// 2. Configuration (Load from Environment Variables)
const baseUrl = process.env.BASEROW_API_URL;
const targetTable = process.env.YOUR_TARGET_TABLE_ID;
try {
// 3. Send to Baserow
const res = await fetch(`${baseUrl}/api/database/rows/table/${targetTable}/?user_field_names=true`, {
method: "POST",
headers: {
"Authorization": `Token ${process.env.BASEROW_API_TOKEN}`,
"Content-Type": "application/json",
},
// ---------------------------------------------------------
// CUSTOMIZATION ZONE: Map your data here
// Left Side: Exact Column Name in Baserow (Case Sensitive)
// Right Side: The variable from step 1
// ---------------------------------------------------------
body: JSON.stringify({
"Name": formField1,
"Email": formField2,
"Status": "New Submission"
}),
});
if (!res.ok) {
throw new Error(`Baserow rejected the data: ${res.statusText}`);
}
} catch (error) {
console.error("Submission Failed:", error);
throw error; // This will trigger your error boundary or UI state
}
// 4. Success Action (Redirect or Refresh)
// redirect("/success");
}
You need to change the body: JSON.stringify({...}) and YOUR_TARGET_TABLE_ID to fit your use case.
B: Wiring it to your UI
Now that you have the infrastructure, you just need to trigger it. You don’t need complex API calls or useEffect hooks.
Find your existing HTML <form> tag and simply point the action attribute to the function you created above.
// Your existing UI code...
return (
<div className="my-custom-form">
<h1>Apply Now</h1>
{/* Connect the form to the Server Action */}
<form action={submitToBaserow} className="flex flex-col gap-4">
{/* The 'name' attribute must match what you look for in Step 1 */}
<input name="myFieldName" type="text" placeholder="Name" />
<input name="myOtherFieldName" type="email" placeholder="Email" />
<button type="submit">Submit Application</button>
</form>
</div>
);
The code above uses method: 'POST' which creates a new row.
If you want to update an existing row (e.g., marking a task as “Done”), you need to change your API Route:
POST to PATCH..../rows/table/${targetTable}/${rowId}/
.../rows/table/123/.../rows/table/123/45/ (where 45 is the specific row to update).To upload files to Baserow via API,
POSTthe binary file toapi/user-files/upload-file/to get a file token, thenPOSTthat token to the row creation endpoint.
By default, Next.js caches your content for speed. If you prefer your live site to update immediately whenever you change data in Baserow (instead of waiting for a cache refresh), you can use Vercel Deploy Hooks.
How to set it up:
Row Created, Row Updated, and Row Deleted.Now, every edit in Baserow forces Vercel to rebuild your site instantly.
This is suitable for low-traffic databases. Using Vercel Deploy Hooks triggers a full site rebuild. If your Baserow database changes frequently (e.g., 50 edits an hour), you will rebuild your site 50 times an hour, potentially slowing down the deployment queue.
Baserow’s API returns the full row data by default. If your table has sensitive data, you must ensure this data never reaches the client’s browser.
Since Vercel runs server-side, you should sanitize the response in your library file before returning it to the frontend component.
return data.results.map(job => ({
title: job.Title,
description: job.Description,
slug: job.id.toString()
// Salary and Notes are explicitly excluded
}));
High-traffic enterprise sites should not hit the Baserow API on every single page view.
next: { revalidate: 60 } in our fetch call. The site is instant for users, but Vercel checks Baserow for updates in the background every 60 seconds.If your Baserow table contains images, the API returns a direct URL. Do not use the standard HTML <img> tag.
Use the Next.js <Image /> component. This automatically resizes, compresses, and converts images to modern formats (like WebP) on the fly.
Configuration:
You must add your Baserow domain to next.config.js to allow this optimization:
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'files.baserow.io', // or your self-hosted domain
},
],
},
For content editors, waiting 60 seconds for ISR can be annoying. You can configure Vercel Preview Mode to bypass the cache and fetch live data from Baserow only for logged-in admin users. This gives you a Staging environment without needing a separate deployment.
Baserow is a relational database. When customizing the body: JSON.stringify section to send data to Baserow, keep these rules in mind:
Linked Records (Relations)
If you are linking this new row to an existing row (e.g., linking an Applicant to a Position), Baserow expects an Array of IDs, not a single number or text: "Applying for": [15]
Select Fields
If you are writing to a “Single Select” field (like “Status”), you must send the text exactly as it appears in Baserow options (Case Sensitive), OR use the Select Option ID: "Status": "Active"
Field Names:
Always ensure the keys on the left side of your JSON (e.g., "Name", "Email") match your Baserow column headers exactly. If you renamed “Name” to “Full Name” in Baserow, you must update your code to "Full Name": formField1.
By connecting Vercel and Baserow via REST API, you create a separation of concerns:
Files Changed:
lib/api.ts: Updated to include the universal fetcher and headers.src/app/page.tsx: Updated to read and display the list of records.src/app/posts/[slug]/page.tsx: Updated to read a single record and handle the “Write” operation via Server Action.| Error | Likely Cause | The Fix |
|---|---|---|
401 Unauthorized |
Missing or malformed Token. | Ensure the header is Authorization: Token [KEY]. |
404 Not Found |
Wrong Table ID or Row ID. | Check the URL of your Baserow table for the number. |
400 Bad Request |
Field Name mismatch. | Ensure the JSON keys match your Baserow column names exactly (Case Sensitive). |
Still need help? If you’re looking for something else, please feel free to make recommendations or ask us questions; we’re ready to assist you.