Skip to content

Commit cd7d2ed

Browse files
committed
chore(examples): multitenant deploys example
1 parent b9cc460 commit cd7d2ed

File tree

7 files changed

+952
-10
lines changed

7 files changed

+952
-10
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: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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 18 or higher
17+
- Yarn package manager
18+
- Rivet CLI installed
19+
- A Rivet account with a project and environment
20+
21+
### Environment Variables
22+
23+
You'll need to set the following environment variables:
24+
25+
```bash
26+
RIVET_CLOUD_TOKEN=your_rivet_token
27+
RIVET_PROJECT=your_project_id
28+
RIVET_ENVIRONMENT=your_environment_name
29+
PORT=3000 # Optional, defaults to 3000
30+
```
31+
32+
### Installation
33+
34+
```bash
35+
# Install dependencies
36+
yarn install
37+
38+
# Start the server
39+
yarn dev
40+
```
41+
42+
### Testing
43+
44+
```bash
45+
yarn test
46+
```
47+
48+
## API Usage
49+
50+
### Deploy Endpoint
51+
52+
`POST /deploy`
53+
54+
**Request:**
55+
- Multipart form data
56+
- Required fields:
57+
- `appId`: Unique identifier for the application (3-63 characters, alphanumeric with hyphens)
58+
- `Dockerfile`: A valid Dockerfile for the application
59+
- Additional files for the application
60+
61+
**Response:**
62+
```json
63+
{
64+
"success": true,
65+
"appId": "your-app-id",
66+
"hostname": "your-app-id.example.com",
67+
"publishOutput": "Output from rivet publish",
68+
"routeUpdate": { /* Response from Rivet routes API */ }
69+
}
70+
```
71+
72+
**Error Response:**
73+
```json
74+
{
75+
"error": "Error message",
76+
"message": "Detailed error message"
77+
}
78+
```
79+
80+
## Example Usage
81+
82+
```bash
83+
curl -X POST http://localhost:3000/deploy \
84+
-F "appId=my-app" \
85+
-F "Dockerfile=@./Dockerfile" \
86+
-F "index.js=@./index.js" \
87+
-F "package.json=@./package.json"
88+
```
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: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { serve } from "@hono/node-server";
2+
import { Hono } from "hono";
3+
import { exec } from "node:child_process";
4+
import { promisify } from "node:util";
5+
import * as fs from "node:fs/promises";
6+
import * as path from "node:path";
7+
import temp from "temp";
8+
import { RivetClient } from "@rivet-gg/api-full";
9+
10+
// Auto-track and cleanup temp directories/files
11+
temp.track();
12+
const execAsync = promisify(exec);
13+
14+
// Config (should be moved to env vars in production)
15+
const PORT = process.env.PORT || 3000;
16+
const RIVET_CLOUD_TOKEN = process.env.RIVET_CLOUD_TOKEN;
17+
const RIVET_PROJECT = process.env.RIVET_PROJECT;
18+
const RIVET_ENVIRONMENT = process.env.RIVET_ENVIRONMENT;
19+
20+
if (!RIVET_CLOUD_TOKEN || !RIVET_PROJECT || !RIVET_ENVIRONMENT) {
21+
console.error(
22+
"Missing required environment variables: RIVET_CLOUD_TOKEN, RIVET_PROJECT, RIVET_ENVIRONMENT",
23+
);
24+
process.exit(1);
25+
}
26+
27+
const rivet = new RivetClient({ token: RIVET_CLOUD_TOKEN });
28+
29+
const app = new Hono();
30+
31+
app.onError((err, c) => {
32+
console.error("Error during operation:", err);
33+
return c.json(
34+
{
35+
error: "Operation failed",
36+
message: err instanceof Error ? err.message : String(err),
37+
},
38+
500,
39+
);
40+
});
41+
42+
app.get("/", (c) => {
43+
return c.text("Multitenant Deploy Service");
44+
});
45+
46+
app.post("/deploy", async (c) => {
47+
// Get the form data
48+
const formData = await c.req.formData();
49+
const appId = formData.get("appId");
50+
51+
if (!appId || typeof appId !== "string") {
52+
return c.json({ error: "Missing or invalid appId" }, 400);
53+
}
54+
55+
// Validate app ID (alphanumeric and hyphens only, 3-30 chars)
56+
if (!/^[a-z0-9-]{3,30}$/.test(appId)) {
57+
return c.json(
58+
{
59+
error: "Invalid appId format. Must be 3-30 characters, lowercase alphanumeric with hyphens.",
60+
},
61+
400,
62+
);
63+
}
64+
65+
// Create a temp directory for the files
66+
const tempDir = await temp.mkdir("rivet-deploy-");
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(tempDir, fieldName);
75+
76+
// Ensure parent directory exists
77+
await fs.mkdir(path.dirname(filePath), { recursive: true });
78+
79+
// Write the file
80+
await fs.writeFile(filePath, Buffer.from(await value.arrayBuffer()));
81+
82+
// Check if this is a Dockerfile
83+
if (fieldName === "Dockerfile" || fieldName.endsWith("/Dockerfile")) {
84+
hasDockerfile = true;
85+
}
86+
}
87+
88+
if (!hasDockerfile) {
89+
return c.json({ error: "Dockerfile is required" }, 400);
90+
}
91+
92+
// Run rivet publish command
93+
console.log(`Publishing app ${appId} from ${tempDir}...`);
94+
const tags = {
95+
// Differentiates from other actor types
96+
kind: "app",
97+
// Specify which app this is
98+
app: appId,
99+
};
100+
const publishResult = await execAsync(
101+
`RIVET_CLOUD_TOKEN=${RIVET_CLOUD_TOKEN} rivet publish ${appId} ${tempDir} -e ${RIVET_ENVIRONMENT}`,
102+
{ maxBuffer: 10 * 1024 * 1024 }, // 10MB buffer for output
103+
);
104+
105+
console.log("Publish output:", publishResult.stdout);
106+
107+
// TODO: Create containers
108+
// TODO: Attach env vars
109+
110+
// Update the route
111+
const hostname = `${appId}.rivet.run`;
112+
rivet.routes.update(`app-${appId}`, {
113+
project: RIVET_PROJECT,
114+
environment: RIVET_ENVIRONMENT,
115+
body: {
116+
hostname,
117+
path: "/",
118+
stripPrefix: false,
119+
routeSubpaths: true,
120+
target: { actors: { selectorTags: tags } },
121+
},
122+
});
123+
124+
return c.json({
125+
success: true,
126+
appId,
127+
hostname,
128+
});
129+
});
130+
131+
console.log(`Server starting on port ${PORT}...`);
132+
serve({
133+
fetch: app.fetch,
134+
port: Number(PORT),
135+
});

0 commit comments

Comments
 (0)