Skip to main content

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:

An demo of the app we built where a scatter plot updates in real-time

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.

  1. Use the create-hono starter to create a new Hono.js project named realtime-app with the cloudflare-workers template and npm as the package manager:

    npm create hono@latest realtime-app -- --template cloudflare-workers --pm npm
  2. 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

  1. Install Prisma CLI as a dev dependency:

    npm install prisma --save-dev
  2. Install the Prisma Accelerate client extension as that's required for Prisma Postgres:

    npm i @prisma/extension-accelerate
  3. Install the Prisma Pulse client extension for real-time database updates:

    npm i @prisma/extension-pulse
  4. Initialize Prisma in your application:

    npx prisma init

This will create:

  • A prisma folder containing schema.prisma, where you will define your database schema.
  • An .env file in the project root, which stores environment variables.
    note

    You 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.

  1. Navigate to .
  2. Click New project and enter a name for your project in the Name field.
  3. Under the Prisma Postgres® section, click Get started.
  4. Select a region close to your location from the Region dropdown.
  5. Click Create project. You’ll be redirected to the database setup page.
  6. In the Set up database access section, copy the DATABASE_URL and PULSE_API_KEY environment variables. These will be required in the next steps.

3.1. Configure development environment variables

  1. In your project root, create a .dev.vars file to store environment variables:

    .dev.vars
    DATABASE_URL=<your-database-url>
    PULSE_API_KEY=<your-pulse-api-key>
  2. Delete the .env file created by Prisma initialization, as .env is not compatible with Cloudflare Workers.

3.2. Update your Prisma schema

  1. Open the schema.prisma file in the prisma folder.

  2. 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.

  1. Install the dotenv-cli package to load environment variables from .dev.vars:

    npm i -D dotenv-cli
  2. Add a migration script to the scripts section of package.json:

    "scripts": {
    "migrate": "dotenv -e .dev.vars -- npx prisma migrate dev"
    // Other scripts created by Hono
    }
  3. Run the migration script to apply changes to the database:

    npm run migrate
  4. When prompted, provide a name for the migration (e.g., init).

  5. 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:

src/index.ts
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:

src/index.ts
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:

src/index.ts
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:

src/index.ts
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:

src/index.ts
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:

An demo of the app we built where a scatter plot updates in real-time

note

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

  1. Use the following command to deploy your project to Cloudflare Workers:

    npm run deploy

    The wrangler CLI will bundle and upload your application.

  2. If you’re not already logged in, the wrangler CLI will open a browser window prompting you to log in to the Cloudflare dashboard.

    note

    If you belong to multiple accounts, select the account where you want to deploy the project.

  3. 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.

  1. Use the npx wrangler secret put command to upload the DATABASE_URL:

    npx wrangler secret put DATABASE_URL

    When prompted, paste the DATABASE_URL value.

  2. 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: