Skip to content

Commit abba1a0

Browse files
committed
chore(examples): multitenant deploys example
1 parent 599e320 commit abba1a0

File tree

16 files changed

+3524
-1812
lines changed

16 files changed

+3524
-1812
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
FROM node:18-alpine
2+
3+
WORKDIR /app
4+
5+
# Install rivet CLI
6+
RUN apk add --no-cache curl unzip
7+
RUN curl -fsSL https://get.rivet.gg/install.sh | sh
8+
9+
# Copy package files and install dependencies
10+
COPY package.json yarn.lock ./
11+
RUN yarn install --frozen-lockfile
12+
13+
# Copy application code
14+
COPY . .
15+
16+
# Build the application
17+
RUN yarn build
18+
19+
# Expose the port the app will run on
20+
EXPOSE 3000
21+
22+
# Create a non-root user
23+
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
24+
USER appuser
25+
26+
# Start the application
27+
CMD ["node", "dist/index.js"]
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# Multitenant Deploys for Rivet
2+
3+
A simple Node.js service for handling multi-tenant deployments with Rivet.
4+
5+
## Features
6+
7+
- Accepts source code uploads via a multipart POST request
8+
- Validates the presence of a Dockerfile
9+
- Deploys the code to Rivet using `rivet publish`
10+
- Sets up a custom domain route for the application
11+
12+
## Getting Started
13+
14+
### Prerequisites
15+
16+
- Node.js
17+
- [Rivet CLI](https://rivet.gg/docs/install)
18+
- Rivet cloud token ([instructions on how to generate](https://rivet.gg/docs/tokens#cloud-token))
19+
- Rivet project ID
20+
- For example if your project is at `https://hub.rivet.gg/projects/foobar`, the ID is `foobar`
21+
- Rivet environment ID
22+
- For example if your environment is at `https://hub.rivet.gg/projects/foobar/environments/prod`, the ID is `prod`
23+
24+
### Environment Variables
25+
26+
You'll need to set the following environment variables:
27+
28+
```bash
29+
RIVET_CLOUD_TOKEN=your_rivet_cloud_token
30+
RIVET_PROJECT=your_project_id
31+
RIVET_ENVIRONMENT=your_environment_name
32+
PORT=3000 # Optional, defaults to 3000
33+
```
34+
35+
You can do this by using [`export`](https://askubuntu.com/a/58828) or [dotenv](https://www.npmjs.com/package/dotenv).
36+
37+
### Developing
38+
39+
```bash
40+
yarn install
41+
yarn dev
42+
```
43+
44+
You can now use `POST http://locahlost:3000/deploy/my-app-id`. Read more about example usage below.
45+
46+
### Testing
47+
48+
```bash
49+
yarn test
50+
```
51+
52+
## API Usage
53+
54+
`POST /deploy/:appId`
55+
56+
**Request:**
57+
- URL Path Parameter:
58+
- `appId`: Unique identifier for the application (3-30 characters, lowercase alphanumeric with hyphens)
59+
- Multipart form data containing:
60+
- `Dockerfile`: A valid Dockerfile for the application (required)
61+
- Additional files for the application
62+
63+
**Response:**
64+
```json
65+
{
66+
"success": true,
67+
"appId": "your-app-id",
68+
"endpoint": "https://your-app-id.example.com"
69+
}
70+
```
71+
72+
## Example Usage
73+
74+
```javascript
75+
const appId = "my-app-id";
76+
77+
// Form data that includes project files
78+
const formData = new FormData();
79+
80+
const serverContent = `
81+
const http = require("http");
82+
const server = http.createServer((req, res) => {
83+
res.writeHead(200, { "Content-Type": "text/plain" });
84+
res.end("Hello from " + process.env.MY_ENV_VAR);
85+
});
86+
server.listen(8080);
87+
`;
88+
const serverBlob = new Blob([serverContent], {
89+
type: "application/octet-stream"
90+
});
91+
formData.append("server.js", serverBlob, "server.js");
92+
93+
const dockerfileContent = `
94+
FROM node:22-alpine
95+
WORKDIR /app
96+
COPY . .
97+
98+
# Set env var from build arg
99+
ARG MY_ENV_VAR
100+
ENV MY_ENV_VAR=$MY_ENV_VAR
101+
102+
# Create a non-root user
103+
RUN addgroup -S rivetgroup && adduser -S rivet -G rivetgroup
104+
USER rivet
105+
106+
CMD ["node", "server.js"]
107+
`;
108+
const dockerfileBlob = new Blob([dockerfileContent], {
109+
type: "application/octet-stream"
110+
});
111+
formData.append("Dockerfile", dockerfileBlob, "Dockerfile");
112+
113+
// Run the deploy
114+
const response = fetch(`http://localhost:3000/deploy/${appId}`, {
115+
method: "POST",
116+
body: formData
117+
});
118+
if (response.ok) {
119+
const { endpoint } = await response.json();
120+
}
121+
```
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "multitenant-deploys",
3+
"packageManager": "[email protected]",
4+
"scripts": {
5+
"dev": "tsx src/index.ts",
6+
"test": "vitest run",
7+
"build": "tsc"
8+
},
9+
"dependencies": {
10+
"@hono/node-server": "^1.7.0",
11+
"@rivet-gg/api-full": "workspace:*",
12+
"axios": "^1.6.7",
13+
"hono": "^4.0.5",
14+
"temp": "^0.9.4"
15+
},
16+
"devDependencies": {
17+
"@types/node": "^20.11.19",
18+
"@types/temp": "^0.9.4",
19+
"tsx": "^4.7.0",
20+
"typescript": "^5.3.3",
21+
"vitest": "^1.2.2"
22+
}
23+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { Hono } from "hono";
2+
import { exec } from "node:child_process";
3+
import { promisify } from "node:util";
4+
import * as fs from "node:fs/promises";
5+
import * as path from "node:path";
6+
import temp from "temp";
7+
import { RivetClient } from "@rivet-gg/api-full";
8+
9+
const execAsync = promisify(exec);
10+
11+
// Auto-track and cleanup temp directories/files
12+
temp.track();
13+
14+
// Config
15+
const RIVET_CLOUD_TOKEN = process.env.RIVET_CLOUD_TOKEN;
16+
const RIVET_PROJECT = process.env.RIVET_PROJECT;
17+
const RIVET_ENVIRONMENT = process.env.RIVET_ENVIRONMENT;
18+
19+
if (!RIVET_CLOUD_TOKEN || !RIVET_PROJECT || !RIVET_ENVIRONMENT) {
20+
throw new Error(
21+
"Missing required environment variables: RIVET_CLOUD_TOKEN, RIVET_PROJECT, RIVET_ENVIRONMENT",
22+
);
23+
}
24+
25+
export const rivet = new RivetClient({ token: RIVET_CLOUD_TOKEN });
26+
27+
export const app = new Hono();
28+
29+
app.onError((err, c) => {
30+
console.error("Error during operation:", err);
31+
return c.json(
32+
{
33+
error: "Operation failed",
34+
message: err instanceof Error ? err.message : String(err),
35+
},
36+
500,
37+
);
38+
});
39+
40+
app.get("/", (c) => {
41+
return c.text("Multitenant Deploy Example");
42+
});
43+
44+
app.post("/deploy/:appId", async (c) => {
45+
const appId = c.req.param("appId");
46+
47+
// Get the form data
48+
const formData = await c.req.formData();
49+
50+
if (!appId || typeof appId !== "string") {
51+
return c.json({ error: "Missing or invalid appId" }, 400);
52+
}
53+
54+
// Validate app ID (alphanumeric and hyphens only, 3-30 chars)
55+
if (!/^[a-z0-9-]{3,30}$/.test(appId)) {
56+
return c.json(
57+
{
58+
error: "Invalid appId format. Must be 3-30 characters, lowercase alphanumeric with hyphens.",
59+
},
60+
400,
61+
);
62+
}
63+
64+
// Create a temp directory for the files
65+
const tempDir = await temp.mkdir("rivet-deploy-");
66+
const tempDirProject = path.join(tempDir, "project");
67+
68+
// Process and save each file
69+
let hasDockerfile = false;
70+
for (const [fieldName, value] of formData.entries()) {
71+
// Skip non-file fields
72+
if (!(value instanceof File)) continue;
73+
74+
const filePath = path.join(tempDirProject, fieldName);
75+
76+
await fs.mkdir(path.dirname(filePath), { recursive: true });
77+
78+
await fs.writeFile(filePath, Buffer.from(await value.arrayBuffer()));
79+
80+
if (fieldName === "Dockerfile") {
81+
hasDockerfile = true;
82+
}
83+
}
84+
85+
if (!hasDockerfile) {
86+
return c.json({ error: "Dockerfile is required" }, 400);
87+
}
88+
89+
// Tags unique to this app's functions
90+
const appTags = {
91+
// Specifies that this app is deployed by a user
92+
type: "user-app",
93+
// Specifies which app this function belongs to
94+
//
95+
// Used for attributing billing & more
96+
app: appId,
97+
};
98+
99+
// Write Rivet config
100+
const functionName = `fn-${appId}`;
101+
const rivetConfig = {
102+
functions: {
103+
[functionName]: {
104+
build_path: "./project/",
105+
dockerfile: "./project/Dockerfile",
106+
build_args: {
107+
// See MY_ENV_VAR build args in Dockerfile
108+
MY_ENV_VAR: "custom env var",
109+
APP_ID: appId,
110+
},
111+
tags: appTags,
112+
route_subpaths: true,
113+
strip_prefix: true,
114+
resources: { cpu: 125, memory: 128 },
115+
// If you want to host at a subpath:
116+
// path: "/foobar"
117+
},
118+
},
119+
};
120+
await fs.writeFile(
121+
path.join(tempDir, "rivet.json"),
122+
JSON.stringify(rivetConfig),
123+
);
124+
125+
// Run rivet publish command
126+
console.log(`Deploying app ${appId} from ${tempDir}...`);
127+
128+
// Run the deploy command
129+
const deployResult = await execAsync(
130+
`rivet deploy --environment ${RIVET_ENVIRONMENT} --non-interactive`,
131+
{
132+
cwd: tempDir,
133+
},
134+
);
135+
136+
console.log("Publish output:", deployResult.stdout);
137+
138+
// Get the function endpoint
139+
const endpointResult = await execAsync(
140+
`rivet function endpoint --environment prod ${functionName}`,
141+
{
142+
cwd: tempDir,
143+
},
144+
);
145+
146+
// Strip any extra text and just get the URL
147+
const endpointUrl = endpointResult.stdout.trim();
148+
console.log("Function endpoint:", endpointUrl);
149+
150+
return c.json({
151+
success: true,
152+
appId,
153+
endpoint: endpointUrl,
154+
});
155+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { serve } from "@hono/node-server";
2+
import { app } from "./app";
3+
4+
const PORT = process.env.PORT || 3000;
5+
console.log(`Server starting on port ${PORT}...`);
6+
serve({
7+
fetch: app.fetch,
8+
port: Number(PORT),
9+
});
10+

0 commit comments

Comments
 (0)