Build a real-time application with Prisma Postgres and Cloudflare Workers
This guide walks you through building a real-time application using Hono.js, Prisma Postgres, and Cloudflare Workers. By the end of this guide, you’ll have a fullstack app where users can submit points (x
and y
coordinates) via a form, visualize the data in a scatter plot, and see updates in real time when new points are added. The final application will look like this:
Here's what you'll learn:
- How to set up a Hono.js project for Cloudflare workers with Prisma ORM.
- How to use real-time features of Prisma Postgres in Hono.js.
- How to deploy the project to Cloudflare.
Prerequisites
To follow this guide, ensure you have the following:
- Node.js version: A compatible Node.js version, required for Prisma 6.
- Accounts:
- Basic knowledge of Cloudflare deployment is recommended for smoother implementation but not mandatory.
1. Set up Hono.js for Cloudflare Workers
Hono.js is a lightweight web framework for building applications optimized for edge environments. Learn more from the official Hono.js Cloudflare Workers guide.
-
Use the
create-hono
starter to create a new Hono.js project namedrealtime-app
with thecloudflare-workers
template andnpm
as the package manager:npm create hono@latest realtime-app -- --template cloudflare-workers --pm npm
-
Agree to install the project dependencies from the previous CLI prompt and then navigate into the newly created app directory:
cd ./realtime-app
2. Set up Prisma in your application
-
Install Prisma CLI as a dev dependency:
npm install prisma --save-dev
-
Install the Prisma Accelerate client extension as that's required for Prisma Postgres:
npm i @prisma/extension-accelerate
-
Install the Prisma Pulse client extension for real-time database updates:
npm i @prisma/extension-pulse
-
Initialize Prisma in your application:
npx prisma init
This will create:
- A
prisma
folder containingschema.prisma
, where you will define your database schema. - An
.env
file in the project root, which stores environment variables.noteYou will not use
.env
files, as they are incompatible with Cloudflare Workers. You will delete this file later.
3. Create a Prisma Postgres instance
To store your app’s data, you’ll create a Prisma Postgres database instance using the Prisma Data Platform.
- Navigate to .
- Click New project and enter a name for your project in the Name field.
- Under the Prisma Postgres® section, click Get started.
- Select a region close to your location from the Region dropdown.
- Click Create project. You’ll be redirected to the database setup page.
- In the Set up database access section, copy the
DATABASE_URL
andPULSE_API_KEY
environment variables. These will be required in the next steps.
3.1. Configure development environment variables
-
In your project root, create a
.dev.vars
file to store environment variables:.dev.varsDATABASE_URL=<your-database-url>
PULSE_API_KEY=<your-pulse-api-key> -
Delete the
.env
file created by Prisma initialization, as.env
is not compatible with Cloudflare Workers.
3.2. Update your Prisma schema
-
Open the
schema.prisma
file in theprisma
folder. -
Add the following model to define the structure of your database:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Points {
id Int @id @default(autoincrement())
x Int
y Int
}
This model defines a Points
table with the fields id
, x
, and y
.
3.3. Apply database schema changes
To update your database with the schema changes, you will create and run a migration.
-
Install the
dotenv-cli
package to load environment variables from.dev.vars
:npm i -D dotenv-cli
-
Add a migration script to the
scripts
section ofpackage.json
:"scripts": {
"migrate": "dotenv -e .dev.vars -- npx prisma migrate dev"
// Other scripts created by Hono
} -
Run the migration script to apply changes to the database:
npm run migrate
-
When prompted, provide a name for the migration (e.g.,
init
). -
Generate
PrismaClient
with the--no-engine
flag, so that it generates a client for an edge runtime:npx prisma generate --no-engine
After the steps above are complete, your Prisma ORM is fully set up and connected to your Postgres database.
4. Develop the application
Now, you will develop a real-time application. The app will let users submit points (x
and y
coordinates) via a simple form and display them in a scatter plot that is updated automatically whenever a new point is added.
4.1. Clear the existing src/index.ts
file
Remove all content from the src/index.ts
file to start with a clean slate. For each of the following steps, append the new code block to the end of index.ts
.
4.2. Set up dependencies and environment bindings
Add the required imports and define environment variable bindings to use the DATABASE_URL
and PULSE_API_KEY
in your application:
import { PrismaClient } from "@prisma/client/edge";
import { withAccelerate } from "@prisma/extension-accelerate";
import { withPulse } from "@prisma/extension-pulse/workerd";
import { Hono } from "hono";
import { upgradeWebSocket } from "hono/cloudflare-workers";
import { requestId } from 'hono/request-id';
// Define environment bindings
type Bindings = {
DATABASE_URL: string;
PULSE_API_KEY: string;
};
const app = new Hono<{ Bindings: Bindings }>();
app.use('*', requestId());
4.3. Create a helper method to use PrismaClient
in the application
Create a helper function to initialize PrismaClient
with the Prisma Accelerate and Pulse client extensions:
const createPrismaClient = (databaseUrl: string, pulseApiKey: string) => {
return new PrismaClient({
datasourceUrl: databaseUrl,
})
.$extends(withAccelerate())
.$extends(
withPulse({
apiKey: pulseApiKey,
})
);
};
4.4. Create a route to establish a WebSocket connection
This route streams updates in real-time when new points are added to the database:
app.get(
"/ws",
upgradeWebSocket(async (c) => {
const prisma = createPrismaClient(c.env.DATABASE_URL, c.env.PULSE_API_KEY);
let listeningToRealtimeStream = false;
return {
onMessage(event, ws) {
if (!listeningToRealtimeStream) {
c.executionCtx.waitUntil(
(async () => {
listeningToRealtimeStream = true;
const pointStream = await prisma.points.stream({
name: `points-stream-${c.get('requestId')}`,
create: {},
});
for await (const event of pointStream) {
ws.send(JSON.stringify({ x: event.created.x, y: event.created.y }));
}
})()
);
}
},
onClose: () => console.log("WebSocket connection closed."),
};
})
);
4.5. Create a POST
route which enables you to save Points
in the database
This route validates user input and saves new points to the database:
app.post("/", async (c) => {
const { x, y } = await c.req.json();
if (typeof x !== "number" || typeof y !== "number") {
return c.text("Invalid input: x and y must be numbers.", 400);
}
const prisma = createPrismaClient(c.env.DATABASE_URL, c.env.PULSE_API_KEY);
const newPoint = await prisma.points.create({ data: { x, y } });
return c.json({ point: newPoint });
});
4.6. Create a GET
route that serves an HTML page
This route serves an HTML page with a form and scatter plot. It also establishes a connection to the WebSocket route and receives and reflects events from Prisma Postgres in real-time:
app.get("/", async (c) => {
const prisma = createPrismaClient(c.env.DATABASE_URL, c.env.PULSE_API_KEY);
const dataPoints = await prisma.points.findMany({
take: 100,
orderBy: { id: "desc" },
select: { x: true, y: true },
}) || [];
return c.html(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Realtime Line Chart</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
html, body {
margin: 0;
padding: 0;
font-family: sans-serif;
height: 100%;
}
.form-container { margin: 1rem; text-align: center; }
.chart-container { display: flex; justify-content: center; min-height: 70vh; }
canvas { max-width: 500px; height: 100%; }
</style>
</head>
<body>
<div class="form-container">
<form id="pointForm">
<input type="number" name="x" placeholder="Enter X" required />
<input type="number" name="y" placeholder="Enter Y" required />
<button type="submit">Add Point</button>
</form>
</div>
<div class="chart-container"><canvas id="myChart"></canvas></div>
<script>
const dataPoints = ${JSON.stringify(dataPoints).replace(/`/g, '\\`')};
const ctx = document.getElementById('myChart').getContext('2d');
const myChart = new Chart(ctx, {
type: 'scatter',
data: {
datasets: [
{
label: \`Points data\`,
data: dataPoints,
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.5)',
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: { type: 'linear', position: 'bottom', title: { display: true, text: 'X Axis' } },
y: { beginAtZero: true, title: { display: true, text: 'Y Axis' } },
},
},
});
const form = document.getElementById('pointForm');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(form);
const x = parseFloat(formData.get('x'));
const y = parseFloat(formData.get('y'));
if (isNaN(x) || isNaN(y)) return alert('Invalid input');
try {
const res = await fetch('/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ x, y }),
});
if (!res.ok) throw new Error('API error');
form.reset();
} catch (err) {
alert('Failed to add point');
}
});
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
const wsUrl = wsProtocol.concat("://").concat(window.location.host).concat("/ws");
const ws = new WebSocket(wsUrl);
ws.onopen = () => {
ws.send('Connect to WebSocket server');
};
ws.onmessage = (event) => {
const point = JSON.parse(event.data);
myChart.data.datasets[0].data.push(point);
myChart.update();
};
ws.onerror = () => alert('WebSocket error');
ws.onclose = () => alert('WebSocket closed');
</script>
</body>
</html>
`);
});
export default app;
4.7. Start the server and test your application
Run the development server:
npm run dev
Visit https://localhost:8787
to see your app in action.
You'll find a form where you can input x
and y
values. Each time you submit the form, the scatter plot should update in real-time to reflect the new data:
You can also add points directly to your Prisma Postgres database from anywhere. For example, use Prisma Studio for Prisma Postgres to enter the x
and y
points, and the scatter plot chart will update instantly.
5. Deploy the application to Cloudflare
Now you'll deploy your real-time application to Cloudflare Workers. This involves uploading your application code and securely configuring your environment variables.
5.1. Deploy the application with Wrangler
-
Use the following command to deploy your project to Cloudflare Workers:
npm run deploy
The
wrangler
CLI will bundle and upload your application. -
If you’re not already logged in, the
wrangler
CLI will open a browser window prompting you to log in to the Cloudflare dashboard.noteIf you belong to multiple accounts, select the account where you want to deploy the project.
-
Once the deployment completes, you’ll see output similar to this:
> deploy
> wrangler deploy --minify
⛅️ wrangler 3.101.0
Total Upload: 243.40 KiB / gzip: 83.31 KiB
Worker Startup Time: 20 ms
Uploaded realtime-app (9.80 sec)
Deployed realtime-app triggers (1.60 sec)
https://realtime-app.workers.dev
Current Version ID: {VERSION_ID}Note the returned URL, such as
https://realtime-app.workers.dev
. This is your live application URL.
5.2. Configure secrets for the application
Your application requires the DATABASE_URL
and PULSE_API_KEY
environment variables to work. These secrets must be securely uploaded to Cloudflare.
-
Use the
npx wrangler secret put
command to upload theDATABASE_URL
:npx wrangler secret put DATABASE_URL
When prompted, paste the
DATABASE_URL
value. -
Similarly, upload the
PULSE_API_KEY
:npx wrangler secret put PULSE_API_KEY
When prompted, paste the
PULSE_API_KEY
value.
5.3. Redeploy the application
After configuring the secrets, redeploy your application to ensure it can access the environment variables:
npm run deploy
5.4. Verify the deployment
Visit the live URL provided in the deployment output, such as https://realtime-app.workers.dev
.
Your application should now be fully functional:
- The form for submitting points should work.
- The scatter plot should display data and update in real time.
If you encounter any issues, ensure the secrets were added correctly and check the deployment logs for errors.
Next steps
Congratulations on building and deploying your real-time application with Prisma Postgres and Cloudflare Workers.
Your app is now live and handles real-time updates using WebSocket support in an edge runtime. To enhance it further:
- Add caching for performance using Prisma Accelerate.
- Explore the Prisma Postgres documentation.