Skip to content

Commit b8fbf7d

Browse files
authored
add Oracle demo app to examples/ (#111)
1 parent 2ebfe3c commit b8fbf7d

29 files changed

Lines changed: 4953 additions & 1 deletion

examples/oracle-notes/.env.example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
FLY_APP=typegres-oracle-notes
2+
FLY_ORACLE_APP=typegres-oracle-notes-db
3+
FLY_REGION=fra
4+
NOTES_DOMAIN=oracle-demo.example.com
5+
6+
# Generate strong values; do not commit the populated .env.
7+
ORACLE_SYS_PASSWORD=change-me
8+
ORACLE_APP_PASSWORD=change-me

examples/oracle-notes/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.env
2+
dist/
3+
dist-server/
4+
node_modules/

examples/oracle-notes/Dockerfile

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
FROM node:22-bookworm-slim AS build
2+
WORKDIR /src
3+
4+
COPY . .
5+
RUN npm ci --legacy-peer-deps \
6+
&& npm run build \
7+
&& npm ci --legacy-peer-deps --prefix examples/oracle-notes \
8+
&& npm run build --prefix examples/oracle-notes \
9+
&& npm prune --omit=dev --prefix examples/oracle-notes \
10+
&& rm -rf examples/oracle-notes/node_modules/typegres \
11+
&& mkdir -p examples/oracle-notes/node_modules/typegres \
12+
&& cp package.json examples/oracle-notes/node_modules/typegres/package.json \
13+
&& cp -r dist examples/oracle-notes/node_modules/typegres/dist
14+
15+
FROM node:22-bookworm-slim AS runtime
16+
ENV NODE_ENV=production PORT=3000
17+
WORKDIR /app
18+
19+
COPY --from=build /src/examples/oracle-notes/dist ./dist
20+
COPY --from=build /src/examples/oracle-notes/dist-server ./dist-server
21+
COPY --from=build /src/examples/oracle-notes/node_modules ./node_modules
22+
COPY --from=build /src/examples/oracle-notes/package.json ./package.json
23+
24+
USER node
25+
EXPOSE 3000
26+
CMD ["node", "dist-server/index.mjs"]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
.git
2+
.claude
3+
.direnv
4+
.exobox
5+
pg_data
6+
node_modules
7+
**/node_modules
8+
**/.wrangler
9+
**/dist
10+
!packages/capnweb/dist
11+
!packages/capnweb/dist/**
12+
**/dist-server
13+
coverage
14+
site/public/typegres.js
15+
site/public/typegres.d.ts

examples/oracle-notes/README.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Oracle Notes
2+
3+
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.
4+
5+
## The data model is the API
6+
7+
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`.
8+
9+
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.
10+
11+
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.
12+
13+
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.
14+
15+
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.
16+
17+
Three properties of this model are particularly useful for agent-authored applications:
18+
19+
1. Clients retain much of SQL's compositional power within the boundaries they have been given.
20+
2. Reads and mutations follow the same authority graph instead of reproducing it across CRUD endpoints and a separate policy layer.
21+
3. The main review surface is one file describing the data, relationships, allowed queries, and mutations—and that file is also the API.
22+
23+
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.
24+
25+
## Local development
26+
27+
From the repository root:
28+
29+
```bash
30+
npm install
31+
npm run build
32+
bin/startora
33+
npm install --prefix examples/oracle-notes
34+
```
35+
36+
Then start the API server and Vite together:
37+
38+
```bash
39+
npm run dev --prefix examples/oracle-notes
40+
```
41+
42+
Development defaults to `oracle://typegres:typegres@localhost:1521/FREEPDB1`. Set `ORACLE_URL` before running the command to override it.
43+
44+
Open <http://localhost:5173>. A new username creates an account; later logins must provide the same password.
45+
46+
## Fly deployment
47+
48+
The demo uses two private-networked Fly apps:
49+
50+
- Node application: standard multi-stage Docker image, HTTP exposed through Fly Proxy.
51+
- Oracle: `gvenzl/oracle-free:23-slim`, private port 1521, one persistent volume. The non-faststart image initializes its database files on the mounted volume.
52+
53+
Copy and edit the deployment configuration:
54+
55+
```bash
56+
cp examples/oracle-notes/.env.example examples/oracle-notes/.env
57+
$EDITOR examples/oracle-notes/.env
58+
examples/oracle-notes/deploy.sh
59+
```
60+
61+
`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.
62+
63+
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.
64+
65+
### Useful commands
66+
67+
```bash
68+
flyctl logs --app "$FLY_APP"
69+
flyctl logs --app "$FLY_ORACLE_APP"
70+
flyctl proxy 1521:1521 --app "$FLY_ORACLE_APP"
71+
```
72+
73+
The Oracle volume is tied to its region and is not replicated. This deployment is a demo, not a production topology.
74+
75+
## Security scope
76+
77+
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.

examples/oracle-notes/deploy.sh

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
HERE="$(cd "$(dirname "$0")" && pwd)"
5+
ROOT="$(cd "$HERE/../.." && pwd)"
6+
if [ -f "$HERE/.env" ]; then
7+
set -a
8+
# shellcheck source=/dev/null
9+
source "$HERE/.env"
10+
set +a
11+
fi
12+
13+
: "${FLY_APP:?Set FLY_APP in examples/oracle-notes/.env}"
14+
: "${FLY_ORACLE_APP:?Set FLY_ORACLE_APP in examples/oracle-notes/.env}"
15+
: "${FLY_REGION:?Set FLY_REGION in examples/oracle-notes/.env}"
16+
: "${NOTES_DOMAIN:?Set NOTES_DOMAIN in examples/oracle-notes/.env}"
17+
: "${ORACLE_SYS_PASSWORD:?Set ORACLE_SYS_PASSWORD in examples/oracle-notes/.env}"
18+
: "${ORACLE_APP_PASSWORD:?Set ORACLE_APP_PASSWORD in examples/oracle-notes/.env}"
19+
if [[ ! "$FLY_REGION" =~ ^[a-z0-9]+$ ]]; then
20+
echo "FLY_REGION must contain only lowercase letters and digits" >&2
21+
exit 1
22+
fi
23+
24+
ensure_app() {
25+
flyctl status --app "$1" >/dev/null 2>&1 || flyctl apps create "$1"
26+
}
27+
ensure_app "$FLY_ORACLE_APP"
28+
ensure_app "$FLY_APP"
29+
30+
if ! flyctl volumes list --app "$FLY_ORACLE_APP" | grep -q 'oracle_data'; then
31+
flyctl volumes create oracle_data --app "$FLY_ORACLE_APP" --region "$FLY_REGION" --size 20 --yes
32+
fi
33+
34+
flyctl secrets set --app "$FLY_ORACLE_APP" \
35+
ORACLE_PASSWORD="$ORACLE_SYS_PASSWORD" \
36+
APP_USER_PASSWORD="$ORACLE_APP_PASSWORD"
37+
flyctl deploy --app "$FLY_ORACLE_APP" --config "$HERE/fly.oracle.toml"
38+
39+
echo "Waiting for Oracle Database to accept connections..."
40+
oracle_deadline=$((SECONDS + 900))
41+
until flyctl ssh console --app "$FLY_ORACLE_APP" \
42+
--command "/opt/oracle/healthcheck.sh" >/dev/null 2>&1; do
43+
if (( SECONDS >= oracle_deadline )); then
44+
echo "Oracle Database did not become ready within 15 minutes." >&2
45+
exit 1
46+
fi
47+
sleep 10
48+
done
49+
50+
encoded_password="$(node -e 'process.stdout.write(encodeURIComponent(process.argv[1]))' "$ORACLE_APP_PASSWORD")"
51+
oracle_url="oracle://typegres_notes:${encoded_password}@${FLY_ORACLE_APP}.internal:1521/FREEPDB1"
52+
flyctl secrets set --app "$FLY_APP" ORACLE_URL="$oracle_url"
53+
(cd "$ROOT" && flyctl deploy --app "$FLY_APP" --config "$HERE/fly.app.toml")
54+
flyctl certs add "$NOTES_DOMAIN" --app "$FLY_APP" || true
55+
56+
echo
57+
printf 'Deployment complete. Configure the DNS records shown by:\n flyctl certs show %q --app %q\n' "$NOTES_DOMAIN" "$FLY_APP"

examples/oracle-notes/fly.app.toml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
primary_region = "fra"
2+
3+
[build]
4+
dockerfile = "Dockerfile"
5+
6+
[env]
7+
PORT = "3000"
8+
ORACLE_POOL_MAX = "8"
9+
10+
[http_service]
11+
internal_port = 3000
12+
force_https = true
13+
auto_stop_machines = "stop"
14+
auto_start_machines = true
15+
min_machines_running = 1
16+
17+
[[http_service.checks]]
18+
interval = "15s"
19+
timeout = "5s"
20+
grace_period = "20s"
21+
method = "GET"
22+
path = "/healthz"
23+
24+
[[vm]]
25+
cpu_kind = "shared"
26+
cpus = 1
27+
memory = "512mb"
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
primary_region = "fra"
2+
3+
[build]
4+
image = "gvenzl/oracle-free:23-slim"
5+
6+
[env]
7+
APP_USER = "typegres_notes"
8+
9+
[mounts]
10+
source = "oracle_data"
11+
destination = "/opt/oracle/oradata"
12+
13+
[checks.oracle]
14+
type = "tcp"
15+
port = 1521
16+
interval = "30s"
17+
timeout = "5s"
18+
grace_period = "10m"
19+
20+
[[vm]]
21+
cpu_kind = "shared"
22+
cpus = 2
23+
memory = "4gb"

examples/oracle-notes/index.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<meta name="theme-color" content="#fffdf7" />
7+
<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" />
8+
<title>Oracle Notes · Typegres</title>
9+
</head>
10+
<body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body>
11+
</html>

0 commit comments

Comments
 (0)