<html lang="en">
<head></head>
<body>

<form id="mainForm" method="post" action="https://stackblitz.com/run" target="_self">
<input type="hidden" name="project[files][README.md]" value="# Redis + Hono Example for RivetKit

Example project demonstrating Redis persistence with Hono web framework and [RivetKit](https://rivetkit.org).

[Learn More →](https://github.com/rivet-gg/rivetkit)

[Discord](https://rivet.gg/discord) — [Documentation](https://rivetkit.org) — [Issues](https://github.com/rivet-gg/rivetkit/issues)

## Getting Started

### Prerequisites

- Node.js 18+
- Redis server running on localhost:6379 (or configure connection)

### Installation

```sh
git clone https://github.com/rivet-gg/rivetkit
cd rivetkit/examples/redis-hono
npm install
```

### Development

Start Redis server (if not already running):
```sh
redis-server
```

Start the RivetKit + Hono server:
```sh
npm run dev
```

In another terminal, run the client demo:
```sh
npm run client
```

Open http://localhost:8088 in your browser to see the API documentation.

## API Endpoints

### Counter API
- `POST /counter/:name/increment` - Increment counter (body: `{amount?: number}`)
- `GET /counter/:name` - Get counter value
- `POST /counter/:name/reset` - Reset counter to 0

### Chat API
- `POST /chat/:room/message` - Send message (body: `{user: string, text: string}`)
- `GET /chat/:room/messages` - Get room messages
- `GET /chat/:room/users` - Get user count in room

### System
- `GET /health` - Health check
- `GET /` - API documentation

## Example Usage

```bash
# Increment a counter
curl -X POST http://localhost:8088/counter/mycounter/increment \
  -H &#39;Content-Type: application/json&#39; \
  -d &#39;{&quot;amount&quot;: 5}&#39;

# Get counter value
curl http://localhost:8088/counter/mycounter

# Send a chat message
curl -X POST http://localhost:8088/chat/general/message \
  -H &#39;Content-Type: application/json&#39; \
  -d &#39;{&quot;user&quot;: &quot;Alice&quot;, &quot;text&quot;: &quot;Hello world!&quot;}&#39;

# Get chat messages
curl http://localhost:8088/chat/general/messages

# Health check
curl http://localhost:8088/health
```

## Configuration

### Environment Variables

- `REDIS_HOST`: Redis server host (default: localhost)
- `REDIS_PORT`: Redis server port (default: 6379)
- `REDIS_PASSWORD`: Redis password (if required)
- `REDIS_DB`: Redis database number (default: 0)

### Example with custom Redis configuration:

```sh
REDIS_HOST=redis.example.com REDIS_PORT=6380 REDIS_PASSWORD=secret npm run dev
```

## Features Demonstrated

- **Redis Persistence**: All actor state persisted in Redis
- **Coordinate Topology**: Multi-node coordination through Redis
- **HTTP API**: RESTful endpoints with Hono framework
- **Real-time State**: Actor state changes broadcast to connected clients
- **Multiple Actors**: Counter and chat room actors in same application
- **Error Handling**: Proper error responses and health checks
- **Connection Management**: User count tracking in chat rooms

## Architecture

This example shows how to build a production-ready API with RivetKit:

1. **RivetKit Core**: Handles actor lifecycle and state management
2. **Redis Drivers**: Persist state and coordinate between server instances
3. **Hono Framework**: Fast HTTP server with clean routing
4. **Actor Pattern**: Encapsulated business logic with actions and events

The coordinate topology allows you to run multiple server instances that will automatically coordinate through Redis, providing horizontal scalability.

## License

Apache 2.0">
<input type="hidden" name="project[files][package.json]" value="{&quot;name&quot;:&quot;example-redis-hono&quot;,&quot;version&quot;:&quot;0.9.9&quot;,&quot;private&quot;:true,&quot;type&quot;:&quot;module&quot;,&quot;scripts&quot;:{&quot;dev&quot;:&quot;tsx --watch src/server.ts&quot;,&quot;check-types&quot;:&quot;tsc --noEmit&quot;,&quot;client&quot;:&quot;tsx scripts/client.ts&quot;},&quot;devDependencies&quot;:{&quot;@types/node&quot;:&quot;^22.13.9&quot;,&quot;@rivetkit/actor&quot;:&quot;https://pkg.pr.new/rivet-gg/rivetkit/@rivetkit/actor@27b9131c5788cdf2007730353b43b33a296aedf3&quot;,&quot;tsx&quot;:&quot;^3.12.7&quot;,&quot;typescript&quot;:&quot;^5.7.3&quot;},&quot;dependencies&quot;:{&quot;@rivetkit/redis&quot;:&quot;https://pkg.pr.new/rivet-gg/rivetkit/@rivetkit/redis@27b9131c5788cdf2007730353b43b33a296aedf3&quot;,&quot;hono&quot;:&quot;4.8.3&quot;,&quot;ioredis&quot;:&quot;^5.4.1&quot;},&quot;stableVersion&quot;:&quot;0.8.0&quot;}">
<input type="hidden" name="project[files][tsconfig.json]" value="{
  &quot;compilerOptions&quot;: {
    &quot;target&quot;: &quot;esnext&quot;,
    &quot;lib&quot;: [&quot;esnext&quot;],
    &quot;module&quot;: &quot;esnext&quot;,
    &quot;moduleResolution&quot;: &quot;bundler&quot;,
    &quot;types&quot;: [&quot;node&quot;],
    &quot;resolveJsonModule&quot;: true,
    &quot;allowJs&quot;: true,
    &quot;checkJs&quot;: false,
    &quot;noEmit&quot;: true,
    &quot;isolatedModules&quot;: true,
    &quot;allowSyntheticDefaultImports&quot;: true,
    &quot;forceConsistentCasingInFileNames&quot;: true,
    &quot;strict&quot;: true,
    &quot;skipLibCheck&quot;: true
  },
  &quot;include&quot;: [&quot;src/**/*&quot;, &quot;scripts/**/*&quot;]
}
">
<input type="hidden" name="project[files][turbo.json]" value="{
  &quot;$schema&quot;: &quot;https://turbo.build/schema.json&quot;,
  &quot;extends&quot;: [&quot;//&quot;]
}
">
<input type="hidden" name="project[files][scripts/client.ts]" value="async function testAPI() {
	const baseUrl = &quot;http://localhost:8088&quot;;

	console.log(&quot;Redis + Hono Example Client&quot;);
	console.log(&quot;===========================&quot;);

	try {
		// Test health endpoint
		console.log(&quot;1. Testing health endpoint...&quot;);
		const healthResponse = await fetch(`${baseUrl}/health`);
		const health = await healthResponse.json();
		console.log(&quot;Health:&quot;, health);

		// Test counter API
		console.log(&quot;\n2. Testing counter API...&quot;);

		// Increment counter
		console.log(&quot;Incrementing counter &#39;demo&#39; by 5...&quot;);
		const incrementResponse = await fetch(`${baseUrl}/counter/demo/increment`, {
			method: &quot;POST&quot;,
			headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },
			body: JSON.stringify({ amount: 5 }),
		});
		const incrementResult = await incrementResponse.json();
		console.log(&quot;Increment result:&quot;, incrementResult);

		// Get counter value
		console.log(&quot;Getting counter value...&quot;);
		const getResponse = await fetch(`${baseUrl}/counter/demo`);
		const getResult = await getResponse.json();
		console.log(&quot;Counter value:&quot;, getResult);

		// Test chat API
		console.log(&quot;\n3. Testing chat API...&quot;);

		// Send messages
		console.log(&quot;Sending messages to chat room &#39;general&#39;...&quot;);

		const messages = [
			{ user: &quot;Alice&quot;, text: &quot;Hello everyone!&quot; },
			{ user: &quot;Bob&quot;, text: &quot;Hi Alice! How are you?&quot; },
			{ user: &quot;Alice&quot;, text: &quot;I&#39;m doing great, thanks!&quot; },
		];

		for (const message of messages) {
			const messageResponse = await fetch(`${baseUrl}/chat/general/message`, {
				method: &quot;POST&quot;,
				headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },
				body: JSON.stringify(message),
			});
			const messageResult = (await messageResponse.json()) as { message: any };
			console.log(`Sent message from ${message.user}:`, messageResult.message);
		}

		// Get messages
		console.log(&quot;\nGetting all messages...&quot;);
		const messagesResponse = await fetch(`${baseUrl}/chat/general/messages`);
		const messagesResult = (await messagesResponse.json()) as {
			messages: any[];
		};
		console.log(&quot;Messages:&quot;, messagesResult.messages);

		// Test multiple counters
		console.log(&quot;\n4. Testing multiple counters...&quot;);

		const counters = [&quot;counter1&quot;, &quot;counter2&quot;, &quot;counter3&quot;];
		for (let i = 0; i &lt; counters.length; i++) {
			const counter = counters[i];
			const amount = (i + 1) * 10;

			const response = await fetch(`${baseUrl}/counter/${counter}/increment`, {
				method: &quot;POST&quot;,
				headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },
				body: JSON.stringify({ amount }),
			});
			const result = (await response.json()) as { count: number };
			console.log(
				`Counter &#39;${counter}&#39; incremented by ${amount}:`,
				result.count,
			);
		}

		// Reset a counter
		console.log(&quot;\n5. Resetting counter &#39;demo&#39;...&quot;);
		const resetResponse = await fetch(`${baseUrl}/counter/demo/reset`, {
			method: &quot;POST&quot;,
		});
		const resetResult = await resetResponse.json();
		console.log(&quot;Reset result:&quot;, resetResult);

		console.log(&quot;\n✅ All tests completed successfully!&quot;);
		console.log(&quot;\nTry these curl commands:&quot;);
		console.log(`curl ${baseUrl}`);
		console.log(`curl ${baseUrl}/health`);
		console.log(
			`curl -X POST ${baseUrl}/counter/test/increment -H &#39;Content-Type: application/json&#39; -d &#39;{&quot;amount&quot;: 5}&#39;`,
		);
		console.log(`curl ${baseUrl}/counter/test`);
	} catch (error) {
		console.error(&quot;❌ Error testing API:&quot;, error);
		console.log(&quot;\nMake sure the server is running with: npm run dev&quot;);
	}
}

testAPI();
">
<input type="hidden" name="project[files][src/registry.ts]" value="import { actor, setup } from &quot;@rivetkit/actor&quot;;

const chatRoom = actor({
	state: {
		messages: [] as Array&lt;{ user: string; text: string; timestamp: number }&gt;,
		userCount: 0,
	},
	actions: {
		sendMessage: (c, message: { user: string; text: string }) =&gt; {
			const newMessage = {
				...message,
				timestamp: Date.now(),
			};
			c.state.messages.push(newMessage);

			// Keep only last 50 messages
			if (c.state.messages.length &gt; 50) {
				c.state.messages = c.state.messages.slice(-50);
			}

			c.broadcast(&quot;newMessage&quot;, newMessage);
			return newMessage;
		},
		getMessages: (c) =&gt; {
			return c.state.messages;
		},
		getUserCount: (c) =&gt; {
			return c.state.userCount;
		},
	},
	onConnect: (c) =&gt; {
		c.state.userCount++;
		c.broadcast(&quot;userCountUpdate&quot;, c.state.userCount);
	},
	onDisconnect: (c) =&gt; {
		c.state.userCount--;
		c.broadcast(&quot;userCountUpdate&quot;, c.state.userCount);
	},
});

const counter = actor({
	state: { count: 0 },
	actions: {
		increment: (c, x: number) =&gt; {
			c.state.count += x;
			c.broadcast(&quot;newCount&quot;, c.state.count);
			return c.state.count;
		},
		getCount: (c) =&gt; {
			return c.state.count;
		},
		reset: (c) =&gt; {
			c.state.count = 0;
			c.broadcast(&quot;newCount&quot;, c.state.count);
			return c.state.count;
		},
	},
});

export const registry = setup({
	use: { chatRoom, counter },
});
">
<input type="hidden" name="project[files][src/server.ts]" value="import { createRedisDriver } from &quot;@rivetkit/redis&quot;;
import { Hono } from &quot;hono&quot;;
import Redis from &quot;ioredis&quot;;
import { registry } from &quot;./registry&quot;;

// Configure Redis connection
const redisClient = new Redis({
	host: process.env.REDIS_HOST || &quot;localhost&quot;,
	port: Number.parseInt(process.env.REDIS_PORT || &quot;6379&quot;),
	password: process.env.REDIS_PASSWORD,
	db: Number.parseInt(process.env.REDIS_DB || &quot;0&quot;),
});

// Handle Redis connection events
redisClient.on(&quot;connect&quot;, () =&gt; {
	console.log(&quot;Connected to Redis&quot;);
});

redisClient.on(&quot;error&quot;, (err) =&gt; {
	console.error(&quot;Redis connection error:&quot;, err);
});

// Start RivetKit with Redis drivers
const { client, serve } = registry.createServer({
	driver: createRedisDriver(),
});

// Setup Hono router
const app = new Hono();

// Counter endpoints
app.post(&quot;/counter/:name/increment&quot;, async (c) =&gt; {
	const name = c.req.param(&quot;name&quot;);
	const body = await c.req.json().catch(() =&gt; ({ amount: 1 }));
	const amount = body.amount || 1;

	const counter = client.counter.getOrCreate(name);
	const newCount = await counter.increment(amount);

	return c.json({
		success: true,
		counter: name,
		count: newCount,
		message: `Counter &#39;${name}&#39; incremented by ${amount}`,
	});
});

app.get(&quot;/counter/:name&quot;, async (c) =&gt; {
	const name = c.req.param(&quot;name&quot;);

	const counter = client.counter.getOrCreate(name);
	const count = await counter.getCount();

	return c.json({
		success: true,
		counter: name,
		count,
	});
});

app.post(&quot;/counter/:name/reset&quot;, async (c) =&gt; {
	const name = c.req.param(&quot;name&quot;);

	const counter = client.counter.getOrCreate(name);
	const count = await counter.reset();

	return c.json({
		success: true,
		counter: name,
		count,
		message: `Counter &#39;${name}&#39; reset`,
	});
});

// Chat room endpoints
app.post(&quot;/chat/:room/message&quot;, async (c) =&gt; {
	const room = c.req.param(&quot;room&quot;);
	const body = await c.req.json();

	if (!body.user || !body.text) {
		return c.json({ error: &quot;Missing user or text&quot; }, 400);
	}

	const chatRoom = client.chatRoom.getOrCreate(room);
	const message = await chatRoom.sendMessage({
		user: body.user,
		text: body.text,
	});

	return c.json({
		success: true,
		room,
		message,
	});
});

app.get(&quot;/chat/:room/messages&quot;, async (c) =&gt; {
	const room = c.req.param(&quot;room&quot;);

	const chatRoom = client.chatRoom.getOrCreate(room);
	const messages = await chatRoom.getMessages();

	return c.json({
		success: true,
		room,
		messages,
	});
});

app.get(&quot;/chat/:room/users&quot;, async (c) =&gt; {
	const room = c.req.param(&quot;room&quot;);

	const chatRoom = client.chatRoom.getOrCreate(room);
	const userCount = await chatRoom.getUserCount();

	return c.json({
		success: true,
		room,
		userCount,
	});
});

// Health check
app.get(&quot;/health&quot;, async (c) =&gt; {
	try {
		await redisClient.ping();
		return c.json({
			status: &quot;healthy&quot;,
			redis: &quot;connected&quot;,
			timestamp: new Date().toISOString(),
		});
	} catch (error) {
		return c.json(
			{
				status: &quot;unhealthy&quot;,
				redis: &quot;disconnected&quot;,
				error: error instanceof Error ? error.message : &quot;Unknown error&quot;,
				timestamp: new Date().toISOString(),
			},
			500,
		);
	}
});

// API documentation
app.get(&quot;/&quot;, (c) =&gt; {
	return c.json({
		message: &quot;RivetKit Redis + Hono Example API&quot;,
		endpoints: {
			counter: {
				&quot;POST /counter/:name/increment&quot;:
					&quot;Increment counter (body: {amount?: number})&quot;,
				&quot;GET /counter/:name&quot;: &quot;Get counter value&quot;,
				&quot;POST /counter/:name/reset&quot;: &quot;Reset counter to 0&quot;,
			},
			chat: {
				&quot;POST /chat/:room/message&quot;:
					&quot;Send message (body: {user: string, text: string})&quot;,
				&quot;GET /chat/:room/messages&quot;: &quot;Get room messages&quot;,
				&quot;GET /chat/:room/users&quot;: &quot;Get user count&quot;,
			},
			system: {
				&quot;GET /health&quot;: &quot;Health check&quot;,
				&quot;GET /&quot;: &quot;This documentation&quot;,
			},
		},
		examples: {
			&quot;Increment counter&quot;:
				&quot;curl -X POST http://localhost:8088/counter/test/increment -H &#39;Content-Type: application/json&#39; -d &#39;{\&quot;amount\&quot;: 5}&#39;&quot;,
			&quot;Send message&quot;:
				&#39;curl -X POST http://localhost:8088/chat/general/message -H \&#39;Content-Type: application/json\&#39; -d \&#39;{&quot;user&quot;: &quot;Alice&quot;, &quot;text&quot;: &quot;Hello world!&quot;}\&#39;&#39;,
		},
	});
});

serve(app);

console.log(
	&quot;RivetKit + Hono server with Redis backend started on http://localhost:8088&quot;,
);
console.log(&quot;Try: curl http://localhost:8088&quot;);
">
<input type="hidden" name="project[description]" value="generated by https://pkg.pr.new">
<input type="hidden" name="project[template]" value="node">
<input type="hidden" name="project[title]" value="example-redis-hono">
</form>
<script>document.getElementById("mainForm").submit();</script>

</body></html>