
or the disappeared middleware!
They need our help and we're not likely to offer it. We are hide bound and stuck in our ways. They have read everything and are just waiting to finish our sentences — but we are not saying the right thing.
Consider a full-stack developer — SQL, Python, JavaScript — then add the libraries like Vue.js, SQLAlchemy, and Tornado, and you have a sense of achievement and a tendency to over-abstract, use patterns, and write reams of boilerplate code. Do you use REST or GraphQL? Do you have a strong desire to automate your CRUD?
You are the last person to talk to a young, knowledgeable developer — you'll just be having them do all your typing and the tests first! You'll try to get them to do what you never did.
What if there was another way? What if you looked carefully at what you have done for the past 15 years and realised that you have been doing it wrong? Do you use stored procedures? Do your solutions require Redis?
How should our coding evolve now that we have access to a truly remarkable resource?
I want to build a simple argument that brings you with me each step of the way. But I will stumble and lose you with my bad writing. So I'm going to place the stack on the table and we can pick up pieces and consider them rather than browbeat you each step of the way.
bun.sh is a fast javascript runtime with special emphasis on websockets and Postgres. At least, that is what their home page says. In fact, we can't use their Postgres as it does not support NOTIFY and LISTEN, so we'll use the npm package pg. The server will handle json-rpc websockets, serve a static folder and rebroadcast NOTIFY messages to all connected clients.
Postgres is now more popular than MySQL — so what can it do? The answer is a lot, and there are many providers out there. It supports stored procedures/functions and triggers. With NOTIFY and LISTEN we can provide realtime updates to clients. During development we can use Docker and a collection of init files to set up the database really speedily.
services:
postgres:
image: postgres:latest
restart: unless-stopped
environment:
POSTGRES_DB: bpv
POSTGRES_USER: postgres
POSTGRES_PASSWORD: secret
ports:
- "5432:5432"
volumes:
- ./init_d:/docker-entrypoint-initdb.d
Placing any .sql file in the init_d directory will be executed on first run. Using -v in your docker compose down removes all volumes. Using -d in your docker compose up runs the database in the background.
With a simple docker compose down -v && docker compose up -d command in your package.json as a script and each change of the init files needs only a single command to stop, reset and restart your database.
Vue.js is a light, reactive framework that can grow to corporate size or stay a hobby. You choose. If you use Vite to build your project or bun, it's still an excellent framework. It keeps JavaScript, HTML, and CSS separate, letting each express itself, with minor additions to the HTML attributes to bind the data. Yup, I don't like React. Three distinct jobs, done by three distinct languages. That's okay; you can use React…
So we open a websocket and create a proxy object to marshall our calls to our functions in the database. The result of every call will be a table — it's sql.
So, using a composable, we can turn a url into a status ref and an api object. Then we can call any of our stored functions through the api object:
import { ref } from 'vue';
import { useRpc } from './rpc.js';
const { api, status } = useRpc();
const term = ref('');
const users = ref([])
const get_users = async () => {
try {
users.value = await api.get_users(`${term.value || ''}%`);
} catch(error){
console.error(error);
}
}
And our stored function declared in our init_d/get_users.sql:
CREATE OR REPLACE FUNCTION get_users(term text)
RETURNS TABLE(id int, name text, email text) AS $$
BEGIN
RETURN QUERY
SELECT id, name, email
FROM users
WHERE name ILIKE term;
END;
$$ LANGUAGE plpgsql;
When data changes our notification callback will be called. The payload is a simple table/operation/data record of the event.
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.method === "notification") {
// Handle real-time incremental notifications from PostgreSQL
const { payload } = message.params;
const { table, operation, record, id } = payload;
// Only handle tasks table notifications
if (table !== "tasks") return;
switch (operation) {
case "INSERT":
tasks.value.push(record);
break;
case "UPDATE":
const index = tasks.value.findIndex((t) => t.id === record.id);
if (index !== -1) {
tasks.value[index] = record;
}
break;
case "DELETE":
tasks.value = tasks.value.filter((t) => t.id !== id);
break;
}
}
};
So our client functions handle errors, not necessarily the data that comes back from insert/update/delete operations. Now our data is live — real-time — if someone else changes data, we get notified.
I have not described authentication since there are so many routes one can take. In a recent project I coded, the only functions that do not take a user_id as the first parameter are login and register. This enables the stored function to access the user's permissions before continuing with the operation. Cookies still work with websockets.
Tests are simple to write. You can test the logic by directly calling the stored functions with bun test and then test the ui using vitest. If you use pinia for state management in the client then the rpc websocket can be a plugin to the stores.
import { test, expect } from "bun:test";
import { Client } from "pg";
const pgClient = new Client({
connectionString: "postgresql://postgres:secret@localhost:5432/bpv",
});
async function run_query(method, params = []) {
const paramPlaceholders = params
.map((_, index) => `$${index + 1}`)
.join(", ");
const query = `SELECT * FROM ${method}(${paramPlaceholders})`;
const result = await pgClient.query(query, params);
return result.rows;
}
test("database operations", async () => {
await pgClient.connect();
const result = await run_query(`save_task`, [
'{"title": "make tea", "description": "earl grey, weak.", "completed": false}',
]);
console.log("result", result);
expect(result).toBeDefined();
expect(result[0]).toHaveProperty("id");
});
Our coding buddies need only worry about the database and then the ui to express it. Two tiers, not three tears. A focused prompt will result in a working solution. I use Claude 4.0 in Zed and often create a file and use the in-file prompt to build each step and only resort to the chat panel to coordinate multiple changes. Like Picard, I regularly say "Make it so" after flipping from ask to write. Gemini is also brilliant, but costly.
Stored functions, reactive state; the rest is plumbing — it's disappeared.