Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions examples/oracle-notes/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FLY_APP=typegres-oracle-notes
FLY_ORACLE_APP=typegres-oracle-notes-db
FLY_REGION=fra
NOTES_DOMAIN=oracle-demo.example.com

# Generate strong values; do not commit the populated .env.
ORACLE_SYS_PASSWORD=change-me
ORACLE_APP_PASSWORD=change-me
4 changes: 4 additions & 0 deletions examples/oracle-notes/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.env
dist/
dist-server/
node_modules/
26 changes: 26 additions & 0 deletions examples/oracle-notes/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
FROM node:22-bookworm-slim AS build
WORKDIR /src

COPY . .
RUN npm ci --legacy-peer-deps \
&& npm run build \
&& npm ci --legacy-peer-deps --prefix examples/oracle-notes \
&& npm run build --prefix examples/oracle-notes \
&& npm prune --omit=dev --prefix examples/oracle-notes \
&& rm -rf examples/oracle-notes/node_modules/typegres \
&& mkdir -p examples/oracle-notes/node_modules/typegres \
&& cp package.json examples/oracle-notes/node_modules/typegres/package.json \
&& cp -r dist examples/oracle-notes/node_modules/typegres/dist

FROM node:22-bookworm-slim AS runtime
ENV NODE_ENV=production PORT=3000
WORKDIR /app

COPY --from=build /src/examples/oracle-notes/dist ./dist
COPY --from=build /src/examples/oracle-notes/dist-server ./dist-server
COPY --from=build /src/examples/oracle-notes/node_modules ./node_modules
COPY --from=build /src/examples/oracle-notes/package.json ./package.json

USER node
EXPOSE 3000
CMD ["node", "dist-server/index.mjs"]
15 changes: 15 additions & 0 deletions examples/oracle-notes/Dockerfile.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.git
.claude
.direnv
.exobox
pg_data
node_modules
**/node_modules
**/.wrangler
**/dist
!packages/capnweb/dist
!packages/capnweb/dist/**
**/dist-server
coverage
site/public/typegres.js
site/public/typegres.d.ts
77 changes: 77 additions & 0 deletions examples/oracle-notes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Oracle Notes

This is a deliberately small notes app running on Oracle Database 23. The app itself is basic: create notes, edit them, and delete them. The interesting part is how it is implemented.

## The data model is the API

In Typegres, the backend data model is also the application API. The model in [`server/api.ts`](server/api.ts) has only two tables: `Users` and `Notes`. Members are selectively exposed as capabilities. A note exposes the fields the browser needs, while a user never exposes `password_hash`.

The browser starts with a root `Api` capability. Each HTTP batch logs in and pipelines the resulting `Users` capability through the rest of the operation. From that user, the browser can follow `Users.notes()`, a `Relation.has` edge containing only that user's notes.

The browser then composes ordinary typed query operations over the relation it can reach. For example, the notes list adds `orderBy()` and `select()` on the frontend. That query expression travels over Cap'n Web RPC, Typegres compiles it on the server, and Oracle executes the resulting SQL.

This means a feature such as title search belongs in the frontend query: add a search input and conditionally add a `where()` clause to the existing notes relation. Oracle's operators and built-in functions remain available with TypeScript types, and the filter is pushed down to Oracle. The client can author a new database query without adding a server endpoint or changing the authority boundary.

Mutations follow the same graph. The user capability exposes note creation and supplies `this.id` as the new note's `user_id`. Once the frontend reaches one note through the user's relation, that note grants the ability to update or delete itself.

Three properties of this model are particularly useful for agent-authored applications:

1. Clients retain much of SQL's compositional power within the boundaries they have been given.
2. Reads and mutations follow the same authority graph instead of reproducing it across CRUD endpoints and a separate policy layer.
3. The main review surface is one file describing the data, relationships, allowed queries, and mutations—and that file is also the API.

That is the durable core: the application and its authority model remain separate from the many clients or agent-authored interfaces that may use it.

## Local development

From the repository root:

```bash
npm install
npm run build
bin/startora
npm install --prefix examples/oracle-notes
```

Then start the API server and Vite together:

```bash
npm run dev --prefix examples/oracle-notes
```

Development defaults to `oracle://typegres:typegres@localhost:1521/FREEPDB1`. Set `ORACLE_URL` before running the command to override it.

Open <http://localhost:5173>. A new username creates an account; later logins must provide the same password.

## Fly deployment

The demo uses two private-networked Fly apps:

- Node application: standard multi-stage Docker image, HTTP exposed through Fly Proxy.
- Oracle: `gvenzl/oracle-free:23-slim`, private port 1521, one persistent volume. The non-faststart image initializes its database files on the mounted volume.

Copy and edit the deployment configuration:

```bash
cp examples/oracle-notes/.env.example examples/oracle-notes/.env
$EDITOR examples/oracle-notes/.env
examples/oracle-notes/deploy.sh
```

`NOTES_DOMAIN` is intentionally deployment configuration. After deployment, `flyctl certs show` prints the A/AAAA or CNAME records to add at the domain's current DNS provider; Fly provisions and renews TLS.

The Oracle Machine remains running because database cold starts are expensive. The Node Machine also keeps one instance running so the demo does not pay an application cold-start penalty.

### Useful commands

```bash
flyctl logs --app "$FLY_APP"
flyctl logs --app "$FLY_ORACLE_APP"
flyctl proxy 1521:1521 --app "$FLY_ORACLE_APP"
```

The Oracle volume is tied to its region and is not replicated. This deployment is a demo, not a production topology.

## Security scope

The example demonstrates the same login scheme as the chat sample: PBKDF2 claims a username on first login. Each HTTP RPC operation logs in and hydrates the user before following its note relation. Notes are always filtered and mutated by the authenticated user ID on the server. It intentionally omits password reset, rate limiting, lockout, CSRF hardening for cross-origin hosting, and production database operations.
57 changes: 57 additions & 0 deletions examples/oracle-notes/deploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
set -euo pipefail

HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/../.." && pwd)"
if [ -f "$HERE/.env" ]; then
set -a
# shellcheck source=/dev/null
source "$HERE/.env"
set +a
fi

: "${FLY_APP:?Set FLY_APP in examples/oracle-notes/.env}"
: "${FLY_ORACLE_APP:?Set FLY_ORACLE_APP in examples/oracle-notes/.env}"
: "${FLY_REGION:?Set FLY_REGION in examples/oracle-notes/.env}"
: "${NOTES_DOMAIN:?Set NOTES_DOMAIN in examples/oracle-notes/.env}"
: "${ORACLE_SYS_PASSWORD:?Set ORACLE_SYS_PASSWORD in examples/oracle-notes/.env}"
: "${ORACLE_APP_PASSWORD:?Set ORACLE_APP_PASSWORD in examples/oracle-notes/.env}"
if [[ ! "$FLY_REGION" =~ ^[a-z0-9]+$ ]]; then
echo "FLY_REGION must contain only lowercase letters and digits" >&2
exit 1
fi

ensure_app() {
flyctl status --app "$1" >/dev/null 2>&1 || flyctl apps create "$1"
}
ensure_app "$FLY_ORACLE_APP"
ensure_app "$FLY_APP"

if ! flyctl volumes list --app "$FLY_ORACLE_APP" | grep -q 'oracle_data'; then
flyctl volumes create oracle_data --app "$FLY_ORACLE_APP" --region "$FLY_REGION" --size 20 --yes
fi

flyctl secrets set --app "$FLY_ORACLE_APP" \
ORACLE_PASSWORD="$ORACLE_SYS_PASSWORD" \
APP_USER_PASSWORD="$ORACLE_APP_PASSWORD"
flyctl deploy --app "$FLY_ORACLE_APP" --config "$HERE/fly.oracle.toml"

echo "Waiting for Oracle Database to accept connections..."
oracle_deadline=$((SECONDS + 900))
until flyctl ssh console --app "$FLY_ORACLE_APP" \
--command "/opt/oracle/healthcheck.sh" >/dev/null 2>&1; do
if (( SECONDS >= oracle_deadline )); then
echo "Oracle Database did not become ready within 15 minutes." >&2
exit 1
fi
sleep 10
done

encoded_password="$(node -e 'process.stdout.write(encodeURIComponent(process.argv[1]))' "$ORACLE_APP_PASSWORD")"
oracle_url="oracle://typegres_notes:${encoded_password}@${FLY_ORACLE_APP}.internal:1521/FREEPDB1"
flyctl secrets set --app "$FLY_APP" ORACLE_URL="$oracle_url"
(cd "$ROOT" && flyctl deploy --app "$FLY_APP" --config "$HERE/fly.app.toml")
flyctl certs add "$NOTES_DOMAIN" --app "$FLY_APP" || true

echo
printf 'Deployment complete. Configure the DNS records shown by:\n flyctl certs show %q --app %q\n' "$NOTES_DOMAIN" "$FLY_APP"
27 changes: 27 additions & 0 deletions examples/oracle-notes/fly.app.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
primary_region = "fra"

[build]
dockerfile = "Dockerfile"

[env]
PORT = "3000"
ORACLE_POOL_MAX = "8"

[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 1

[[http_service.checks]]
interval = "15s"
timeout = "5s"
grace_period = "20s"
method = "GET"
path = "/healthz"

[[vm]]
cpu_kind = "shared"
cpus = 1
memory = "512mb"
23 changes: 23 additions & 0 deletions examples/oracle-notes/fly.oracle.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
primary_region = "fra"

[build]
image = "gvenzl/oracle-free:23-slim"

[env]
APP_USER = "typegres_notes"

[mounts]
source = "oracle_data"
destination = "/opt/oracle/oradata"

[checks.oracle]
type = "tcp"
port = 1521
interval = "30s"
timeout = "5s"
grace_period = "10m"

[[vm]]
cpu_kind = "shared"
cpus = 2
memory = "4gb"
11 changes: 11 additions & 0 deletions examples/oracle-notes/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#fffdf7" />
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='18' fill='%23ff5d3a'/%3E%3Cpath d='M21 18h24v8H21zm0 13h17v8H21zm0 13h24v8H21z' fill='white'/%3E%3C/svg%3E" />
<title>Oracle Notes · Typegres</title>
</head>
<body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body>
</html>
Loading
Loading