eraspace

Example

The server exports a capsule. The client imports its type, so queries, mutations, arguments and results are inferred.

server/index.ts

import { capsule, table, query, mutation, string } from "eraspace/server";

export default capsule({
  schema: {
    messages: table({ room: string(), body: string() })
      .index("by_room", ["room"]),
  },
  queries: {
    messages: query((ctx, room: string) =>
      ctx.db.messages
        .withIndex("by_room", q => q.eq("room", room))
        .collect()),
  },
  mutations: {
    send: mutation((ctx, room: string, body: string) =>
      ctx.db.messages.insert({ room, body })),
  },
});

client/index.tsx

import { createClient } from "eraspace/client";
import type app from "../server/index";

const client = createClient<typeof app>();

export default function Chat() {
  const messages = client.useQuery("messages", "general"); // live
  const send = client.useMutation("send");
  return <>
    <ul>{messages?.map(m => <li key={m.id}>{m.body}</li>)}</ul>
    <button onClick={() => send("general", "hello")}>Say hello</button>
  </>;
}

project layout

my-app/
  eraspace.json          app name
  server/index.ts        schema, queries, mutations, actions, endpoints
  client/index.tsx       Preact client
  shared/                code used by both sides
  .env.eraspace.server   server-only env, read through ctx.env

Server files are plain ES modules. Relative imports and eraspace/server work; Node APIs and npm modules do not. fetch, URL, timers, text encoding, console and crypto.randomUUID are available.