<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="# Tenant Dashboard for RivetKit

Example project demonstrating multi-tenant organization management with role-based access control using [RivetKit](https://rivetkit.org).

[Learn More →](https://github.com/rivet-dev/rivetkit)

[Discord](https://rivet.dev/discord) — [Documentation](https://rivetkit.org) — [Issues](https://github.com/rivet-dev/rivetkit/issues)

## Getting Started

### Prerequisites

- Node.js 18+

### Installation

```sh
git clone https://github.com/rivet-dev/rivetkit
cd rivetkit/examples/tenant
npm install
```

### Development

```sh
npm run dev
```

Open your browser to `http://localhost:3000`

## Features

- **Role-Based Access Control**: Different permissions for admin and member roles
- **Multi-Tenant Architecture**: Organization-scoped data and permissions
- **Authentication**: Token-based authentication with connection state
- **Real-time Updates**: Live updates when data changes across clients
- **Permission Enforcement**: Server-side permission checks for all operations
- **User Management**: Admin can add members and update roles
- **Invoice Management**: Admin-only access to billing information
- **Dashboard Analytics**: Role-specific statistics and insights

## How it works

This tenant system demonstrates:

1. **Authentication**: Token-based authentication with `createConnState`
2. **Authorization**: Role-based access control with server-side permission checks
3. **Multi-Tenancy**: Organization-scoped data isolation
4. **Real-time Collaboration**: Live updates across connected clients
5. **Permission Enforcement**: Different UI and API access based on user roles

## Architecture

- **Backend**: RivetKit actor with authentication and role-based permissions
- **Frontend**: React application with conditional rendering based on user roles
- **Authentication**: Token-based with connection state for user context
- **Authorization**: Server-side permission checks for all sensitive operations

## User Roles

### Admin Users
- **Full Access**: Can view all data and perform all operations
- **Member Management**: Add new members and update member roles
- **Invoice Access**: View and manage organization invoices
- **Dashboard Stats**: Access to comprehensive analytics including revenue

### Member Users
- **Limited Access**: Can only view basic organization information
- **Member List**: View team members and their roles
- **Dashboard Stats**: Access to basic member statistics only
- **No Invoice Access**: Cannot view or manage billing information

### Data Isolation
- Organization-scoped data using actor keys
- User context stored in connection state
- Role-based data filtering and access control

## API Endpoints

### Public (All Authenticated Users)
- `getOrganization()` - Get organization information
- `getMembers()` - Get list of all members
- `getCurrentUser()` - Get current user information
- `getDashboardStats()` - Get basic statistics

### Admin Only
- `getInvoices()` - Get all invoices
- `addMember(member)` - Add new member
- `updateMemberRole(memberId, role)` - Update member role
- `markInvoicePaid(invoiceId)` - Mark invoice as paid

## Real-time Updates

The system broadcasts updates to all connected clients:

```typescript
// When member is added
c.broadcast(&quot;memberAdded&quot;, { member: newMember });

// When member role is updated
c.broadcast(&quot;memberUpdated&quot;, { member });

// When invoice is updated
c.broadcast(&quot;invoiceUpdated&quot;, { invoice });
```

## Use Cases

This tenant pattern is perfect for:

- **SaaS Applications**: Multi-tenant software with organization accounts
- **Team Management**: Internal tools with role-based access
- **Project Management**: Collaborative tools with permission levels
- **CRM Systems**: Customer relationship management with user roles
- **Enterprise Software**: Business applications with admin/user hierarchies
- **Learning Management**: Educational platforms with teacher/student roles

## Extending

This tenant system can be enhanced with:

### Advanced Authentication
- **OAuth Integration**: Google, GitHub, Microsoft authentication
- **JWT Tokens**: Stateless authentication with signed tokens
- **Multi-Factor Auth**: SMS, email, or authenticator app verification
- **Session Management**: Secure session handling and expiration

### Enhanced Authorization
- **Custom Roles**: Define custom roles beyond admin/member
- **Permissions**: Granular permissions for specific operations
- **Role Hierarchy**: Nested roles with inheritance
- **Resource-Level Access**: Per-resource permissions

### Multi-Tenancy Features
- **Organization Settings**: Configurable organization preferences
- **Billing Integration**: Stripe, PayPal, or other payment processors
- **Usage Tracking**: Monitor and limit resource usage per tenant
- **Data Export**: Allow tenants to export their data

### Advanced Features
- **Audit Logging**: Track all user actions and changes
- **Activity Feeds**: Real-time activity notifications
- **Team Invitations**: Invite users via email with signup flow
- **API Keys**: Generate API keys for external integrations
- **Webhooks**: Notify external systems of events

## Testing Different Roles

To test the role-based access control:

1. **Login as Alice (Admin)**:
   - Can view members and invoices
   - Can add new members
   - Can update member roles
   - Can mark invoices as paid
   - Sees full dashboard statistics

2. **Login as Bob/Charlie (Member)**:
   - Can view members only
   - Cannot access invoices
   - Cannot manage members
   - Sees limited dashboard statistics
   - Gets permission denied errors for admin operations

## Security Considerations

### Server-Side Validation
- All permission checks happen on the server
- Client-side UI is for user experience only
- Never trust client-side role information

### Token Management
- Use secure token storage (httpOnly cookies in production)
- Implement token refresh mechanisms
- Add token expiration and revocation

### Data Protection
- Sanitize all user inputs
- Use parameterized queries for database operations
- Implement rate limiting for API endpoints
- Log security events and failed authentication attempts

## Performance Considerations

### Caching
- Cache user roles and permissions
- Implement query result caching

### Scalability
- Separate read and write operations
- Use database read replicas for heavy read workloads
- Implement proper indexing for user and organization queries

## License

Apache 2.0
">
<input type="hidden" name="project[files][package.json]" value="{&quot;name&quot;:&quot;example-tenant&quot;,&quot;version&quot;:&quot;2.0.21&quot;,&quot;type&quot;:&quot;module&quot;,&quot;scripts&quot;:{&quot;dev&quot;:&quot;concurrently \&quot;tsx --watch src/backend/server.ts\&quot; \&quot;vite\&quot;&quot;,&quot;build&quot;:&quot;vite build&quot;,&quot;preview&quot;:&quot;vite preview&quot;,&quot;check-types&quot;:&quot;tsc --noEmit&quot;,&quot;test&quot;:&quot;vitest&quot;},&quot;dependencies&quot;:{&quot;rivetkit&quot;:&quot;https://pkg.pr.new/rivet-dev/rivet/rivetkit@09a68bcf14620b1ced7fb1e6f33d3d0503298a6f&quot;,&quot;@rivetkit/react&quot;:&quot;https://pkg.pr.new/rivet-dev/rivet/@rivetkit/react@09a68bcf14620b1ced7fb1e6f33d3d0503298a6f&quot;,&quot;react&quot;:&quot;^18.2.0&quot;,&quot;react-dom&quot;:&quot;^18.2.0&quot;},&quot;devDependencies&quot;:{&quot;@types/node&quot;:&quot;^20.0.0&quot;,&quot;@types/react&quot;:&quot;^18.2.0&quot;,&quot;@types/react-dom&quot;:&quot;^18.2.0&quot;,&quot;@vitejs/plugin-react&quot;:&quot;^4.0.0&quot;,&quot;concurrently&quot;:&quot;^8.2.0&quot;,&quot;tsx&quot;:&quot;^4.0.0&quot;,&quot;typescript&quot;:&quot;^5.0.0&quot;,&quot;vite&quot;:&quot;^5.0.0&quot;,&quot;vitest&quot;:&quot;^3.1.1&quot;}}">
<input type="hidden" name="project[files][tsconfig.json]" value="{
	&quot;compilerOptions&quot;: {
		&quot;target&quot;: &quot;ES2020&quot;,
		&quot;lib&quot;: [&quot;ES2020&quot;, &quot;DOM&quot;, &quot;DOM.Iterable&quot;],
		&quot;module&quot;: &quot;ESNext&quot;,
		&quot;skipLibCheck&quot;: true,
		&quot;moduleResolution&quot;: &quot;bundler&quot;,
		&quot;allowImportingTsExtensions&quot;: true,
		&quot;resolveJsonModule&quot;: true,
		&quot;isolatedModules&quot;: true,
		&quot;noEmit&quot;: true,
		&quot;jsx&quot;: &quot;react-jsx&quot;,
		&quot;strict&quot;: true,
		&quot;noUnusedLocals&quot;: true,
		&quot;noUnusedParameters&quot;: true,
		&quot;noFallthroughCasesInSwitch&quot;: true
	},
	&quot;include&quot;: [&quot;src&quot;, &quot;tests&quot;],
	&quot;exclude&quot;: [&quot;node_modules&quot;, &quot;dist&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][vite.config.ts]" value="import react from &quot;@vitejs/plugin-react&quot;;
import { defineConfig } from &quot;vite&quot;;

export default defineConfig({
	plugins: [react()],
	root: &quot;src/frontend&quot;,
	server: {
		port: 3000,
	},
	build: {
		outDir: &quot;../../dist&quot;,
		emptyOutDir: true,
	},
});
">
<input type="hidden" name="project[files][vitest.config.ts]" value="import { defineConfig } from &quot;vitest/config&quot;;

export default defineConfig({
	test: {
		environment: &quot;node&quot;,
	},
});
">
<input type="hidden" name="project[files][tests/tenant.test.ts]" value="import { setupTest } from &quot;rivetkit/test&quot;;
import { expect, test, vi } from &quot;vitest&quot;;
import { registry } from &quot;../src/backend/registry&quot;;

// Mock authentication function
vi.mock(&quot;../src/backend/registry&quot;, async (importOriginal) =&gt; {
	const mod =
		await importOriginal&lt;typeof import(&quot;../src/backend/registry&quot;)&gt;();
	return {
		...mod,
		// We&#39;ll need to test without connection state since it requires auth
	};
});

test(&quot;Tenant organization can provide basic info&quot;, async (ctx) =&gt; {
	const { client } = await setupTest(ctx, registry);
	const tenant = client.tenant.getOrCreate([&quot;test-org&quot;]);

	// Get organization info
	const orgInfo = await tenant.getOrganization();
	expect(orgInfo).toMatchObject({
		id: expect.any(String),
		name: expect.any(String),
		memberCount: expect.any(Number),
	});
	expect(orgInfo.memberCount).toBeGreaterThan(0);
});

test(&quot;Tenant organization tracks members&quot;, async (ctx) =&gt; {
	const { client } = await setupTest(ctx, registry);
	const tenant = client.tenant.getOrCreate([&quot;test-members&quot;]);

	// Get all members
	const members = await tenant.getMembers();
	expect(Array.isArray(members)).toBe(true);
	expect(members.length).toBeGreaterThan(0);

	// Verify member structure
	members.forEach((member) =&gt; {
		expect(member).toMatchObject({
			id: expect.any(String),
			name: expect.any(String),
			email: expect.any(String),
			role: expect.stringMatching(/^(admin|member)$/),
		});
	});
});

test(&quot;Tenant organization provides dashboard stats&quot;, async (ctx) =&gt; {
	const { client } = await setupTest(ctx, registry);
	const tenant = client.tenant.getOrCreate([&quot;test-stats&quot;]);

	// Get dashboard stats (without admin privileges)
	const stats = await tenant.getDashboardStats();
	expect(stats).toMatchObject({
		totalMembers: expect.any(Number),
		adminCount: expect.any(Number),
		memberCount: expect.any(Number),
	});

	// Verify member counts add up
	expect(stats.adminCount + stats.memberCount).toBe(stats.totalMembers);
	expect(stats.totalMembers).toBeGreaterThan(0);
	expect(stats.adminCount).toBeGreaterThan(0);
});

test(&quot;Tenant organization validates member roles&quot;, async (ctx) =&gt; {
	const { client } = await setupTest(ctx, registry);
	const tenant = client.tenant.getOrCreate([&quot;test-roles&quot;]);

	const members = await tenant.getMembers();
	const orgInfo = await tenant.getOrganization();

	// Verify at least one admin exists
	const admins = members.filter((m) =&gt; m.role === &quot;admin&quot;);
	const regularMembers = members.filter((m) =&gt; m.role === &quot;member&quot;);

	expect(admins.length).toBeGreaterThan(0);
	expect(members.length).toBe(orgInfo.memberCount);
	expect(admins.length + regularMembers.length).toBe(members.length);
});

test(&quot;Tenant organization handles initial data correctly&quot;, async (ctx) =&gt; {
	const { client } = await setupTest(ctx, registry);
	const tenant = client.tenant.getOrCreate([&quot;test-initial-data&quot;]);

	// Verify initial state has expected structure
	const members = await tenant.getMembers();
	const orgInfo = await tenant.getOrganization();

	expect(orgInfo.name).toBeTruthy();
	expect(orgInfo.id).toBeTruthy();
	expect(members.length).toBe(orgInfo.memberCount);

	// Verify we have the expected sample data
	expect(members.some((m) =&gt; m.role === &quot;admin&quot;)).toBe(true);
	expect(members.some((m) =&gt; m.role === &quot;member&quot;)).toBe(true);

	// Verify email formats
	members.forEach((member) =&gt; {
		expect(member.email).toMatch(/@/);
		expect(member.name).toBeTruthy();
		expect(member.id).toBeTruthy();
	});
});

test(&quot;Tenant organization member data consistency&quot;, async (ctx) =&gt; {
	const { client } = await setupTest(ctx, registry);
	const tenant = client.tenant.getOrCreate([&quot;test-consistency&quot;]);

	// Get data multiple times to verify consistency
	const members1 = await tenant.getMembers();
	const members2 = await tenant.getMembers();
	const orgInfo1 = await tenant.getOrganization();
	const orgInfo2 = await tenant.getOrganization();

	expect(members1).toEqual(members2);
	expect(orgInfo1).toEqual(orgInfo2);
	expect(members1.length).toBe(orgInfo1.memberCount);
});
">
<input type="hidden" name="project[files][src/backend/registry.ts]" value="import { actor, setup } from &quot;rivetkit&quot;;

export type Member = {
	id: string;
	name: string;
	email: string;
	role: &quot;admin&quot; | &quot;member&quot;;
};

export type Invoice = {
	id: string;
	amount: number;
	date: number;
	paid: boolean;
	description: string;
};

export type ConnState = {
	userId: string;
	role: &quot;admin&quot; | &quot;member&quot;;
};

const tenant = actor({
	// Persistent state that survives restarts: https://rivet.dev/docs/actors/state
	state: {
		orgId: &quot;org-1&quot;,
		orgName: &quot;Acme Corporation&quot;,
		members: [
			{
				id: &quot;user-1&quot;,
				name: &quot;Alice Johnson&quot;,
				email: &quot;alice@acme.com&quot;,
				role: &quot;admin&quot; as const,
			},
			{
				id: &quot;user-2&quot;,
				name: &quot;Bob Smith&quot;,
				email: &quot;bob@acme.com&quot;,
				role: &quot;member&quot; as const,
			},
			{
				id: &quot;user-3&quot;,
				name: &quot;Charlie Brown&quot;,
				email: &quot;charlie@acme.com&quot;,
				role: &quot;member&quot; as const,
			},
		],
		invoices: [
			{
				id: &quot;inv-001&quot;,
				amount: 1200.0,
				date: Date.now() - 86400000 * 30, // 30 days ago
				paid: true,
				description: &quot;Monthly subscription - Enterprise plan&quot;,
			},
			{
				id: &quot;inv-002&quot;,
				amount: 1200.0,
				date: Date.now() - 86400000 * 7, // 7 days ago
				paid: false,
				description: &quot;Monthly subscription - Enterprise plan&quot;,
			},
			{
				id: &quot;inv-003&quot;,
				amount: 250.0,
				date: Date.now() - 86400000 * 3, // 3 days ago
				paid: true,
				description: &quot;Additional storage - 500GB&quot;,
			},
		],
	},

	actions: {
		// Callable functions from clients: https://rivet.dev/docs/actors/actions
		getOrganization: (c) =&gt; {
			return {
				id: c.state.orgId,
				name: c.state.orgName,
				memberCount: c.state.members.length,
			};
		},

		getMembers: (c) =&gt; {
			return c.state.members;
		},

		getDashboardStats: (c) =&gt; {
			const stats = {
				totalMembers: c.state.members.length,
				adminCount: c.state.members.filter((m) =&gt; m.role === &quot;admin&quot;)
					.length,
				memberCount: c.state.members.filter((m) =&gt; m.role === &quot;member&quot;)
					.length,
			};

			// For testing, always return basic stats
			return stats;
		},
	},
});

// Register actors for use: https://rivet.dev/docs/setup
export const registry = setup({
	use: { tenant },
});
">
<input type="hidden" name="project[files][src/backend/server.ts]" value="import { registry } from &quot;./registry&quot;;

registry.start();
">
<input type="hidden" name="project[files][src/frontend/App.tsx]" value="import { createRivetKit } from &quot;@rivetkit/react&quot;;
import { useEffect, useState } from &quot;react&quot;;
import type { Member, registry } from &quot;../backend/registry&quot;;

const { useActor } = createRivetKit&lt;typeof registry&gt;(&quot;http://localhost:8080&quot;);

const ORG_ID = &quot;org-1&quot;;

export function App() {
	// Authentication state
	const [token, setToken] = useState&lt;string&gt;(&quot;&quot;);
	
	// Data state
	const [organization, setOrganization] = useState&lt;any&gt;(null);
	const [members, setMembers] = useState&lt;Member[]&gt;([]);
	const [dashboardStats, setDashboardStats] = useState&lt;any&gt;(null);
	const [error, setError] = useState&lt;string&gt;(&quot;&quot;);
	const [loading, setLoading] = useState(false);

	// Connect to tenant actor with authentication token
	const tenant = useActor({
		name: &quot;tenant&quot;,
		key: [ORG_ID],
		params: { token },
	});

	// Login functions
	const loginAsAdmin = () =&gt; {
		setToken(&quot;auth:user-1&quot;); // Alice is admin
		setError(&quot;&quot;);
	};

	const loginAsMember = () =&gt; {
		setToken(&quot;auth:user-2&quot;); // Bob is member
		setError(&quot;&quot;);
	};

	const loginAsCharlie = () =&gt; {
		setToken(&quot;auth:user-3&quot;); // Charlie is member
		setError(&quot;&quot;);
	};

	const logout = () =&gt; {
		setToken(&quot;&quot;);
		setOrganization(null);
		setMembers([]);
		setDashboardStats(null);
		setError(&quot;&quot;);
	};

	// Load data when actor is available
	useEffect(() =&gt; {
		if (!tenant.connection || !token) return;

		const loadData = async () =&gt; {
			setLoading(true);
			try {
				// Get organization info
				const org = await tenant.connection!.getOrganization();
				setOrganization(org);

				// Get members (available to all users)
				const membersList = await tenant.connection!.getMembers();
				setMembers(membersList);

				// Get dashboard stats
				const stats = await tenant.connection!.getDashboardStats();
				setDashboardStats(stats);
			} catch (err: any) {
				setError(err.message || &quot;Failed to load data&quot;);
			} finally {
				setLoading(false);
			}
		};

		loadData();
	}, [tenant.connection, token]);

	// Listen for real-time updates
	tenant.useEvent(&quot;memberAdded&quot;, ({ member }: { member: Member }) =&gt; {
		setMembers(prev =&gt; [...prev, member]);
	});

	tenant.useEvent(&quot;memberUpdated&quot;, ({ member }: { member: Member }) =&gt; {
		setMembers(prev =&gt; prev.map(m =&gt; m.id === member.id ? member : m));
	});



	// Login screen when not authenticated
	if (!token) {
		return (
			&lt;div className=&quot;app-container&quot;&gt;
				&lt;div className=&quot;header&quot;&gt;
					&lt;h1&gt;Organization Dashboard&lt;/h1&gt;
					&lt;p&gt;Multi-tenant role-based access control with RivetKit&lt;/p&gt;
				&lt;/div&gt;

				&lt;div className=&quot;info-box&quot;&gt;
					&lt;h3&gt;How it works&lt;/h3&gt;
					&lt;p&gt;
						This tenant system demonstrates role-based access control in a multi-tenant environment. 
						Different user roles have different permissions - admins can access invoices and manage members, 
						while regular members can only view member information.
					&lt;/p&gt;
				&lt;/div&gt;

				&lt;div className=&quot;login-section&quot;&gt;
					&lt;h2&gt;Choose a User to Login&lt;/h2&gt;
					&lt;p&gt;Select a user to see different permission levels:&lt;/p&gt;
					&lt;div className=&quot;login-buttons&quot;&gt;
						&lt;button className=&quot;login-button admin&quot; onClick={loginAsAdmin}&gt;
							Login as Alice (Admin)
						&lt;/button&gt;
						&lt;button className=&quot;login-button member&quot; onClick={loginAsMember}&gt;
							Login as Bob (Member)
						&lt;/button&gt;
						&lt;button className=&quot;login-button member&quot; onClick={loginAsCharlie}&gt;
							Login as Charlie (Member)
						&lt;/button&gt;
					&lt;/div&gt;
				&lt;/div&gt;
			&lt;/div&gt;
		);
	}

	return (
		&lt;div className=&quot;app-container&quot;&gt;
			&lt;div className=&quot;header&quot;&gt;
				&lt;h1&gt;Organization Dashboard&lt;/h1&gt;
				&lt;p&gt;Multi-tenant role-based access control with RivetKit&lt;/p&gt;
			&lt;/div&gt;

			{/* User Info */}
			&lt;div className=&quot;user-info&quot;&gt;
				&lt;div className=&quot;user-details&quot;&gt;
					&lt;span&gt;Logged in&lt;/span&gt;
				&lt;/div&gt;
				&lt;button className=&quot;logout-button&quot; onClick={logout}&gt;
					Logout
				&lt;/button&gt;
			&lt;/div&gt;

			{/* Organization Header */}
			{organization &amp;&amp; (
				&lt;div className=&quot;organization-header&quot;&gt;
					&lt;h2&gt;{organization.name}&lt;/h2&gt;
					&lt;p&gt;Organization ID: {organization.id} • {organization.memberCount} members&lt;/p&gt;
				&lt;/div&gt;
			)}

			{/* Loading State */}
			{loading &amp;&amp; &lt;div&gt;Loading...&lt;/div&gt;}

			{/* Error Display */}
			{error &amp;&amp; (
				&lt;div className=&quot;error-message&quot;&gt;
					&lt;h4&gt;Access Denied&lt;/h4&gt;
					&lt;p&gt;{error}&lt;/p&gt;
				&lt;/div&gt;
			)}

			{/* Dashboard Stats */}
			{dashboardStats &amp;&amp; (
				&lt;div className=&quot;section&quot;&gt;
					&lt;h3&gt;Dashboard Statistics&lt;/h3&gt;
					&lt;div style={{ display: &quot;grid&quot;, gridTemplateColumns: &quot;repeat(auto-fit, minmax(200px, 1fr))&quot;, gap: &quot;20px&quot; }}&gt;
						&lt;div style={{ padding: &quot;20px&quot;, backgroundColor: &quot;#f8f9fa&quot;, borderRadius: &quot;6px&quot;, textAlign: &quot;center&quot; }}&gt;
							&lt;div style={{ fontSize: &quot;24px&quot;, fontWeight: &quot;bold&quot;, color: &quot;#007bff&quot; }}&gt;
								{dashboardStats.totalMembers}
							&lt;/div&gt;
							&lt;div style={{ color: &quot;#6c757d&quot; }}&gt;Total Members&lt;/div&gt;
						&lt;/div&gt;
						&lt;div style={{ padding: &quot;20px&quot;, backgroundColor: &quot;#f8f9fa&quot;, borderRadius: &quot;6px&quot;, textAlign: &quot;center&quot; }}&gt;
							&lt;div style={{ fontSize: &quot;24px&quot;, fontWeight: &quot;bold&quot;, color: &quot;#dc3545&quot; }}&gt;
								{dashboardStats.adminCount}
							&lt;/div&gt;
							&lt;div style={{ color: &quot;#6c757d&quot; }}&gt;Admins&lt;/div&gt;
						&lt;/div&gt;
						&lt;div style={{ padding: &quot;20px&quot;, backgroundColor: &quot;#f8f9fa&quot;, borderRadius: &quot;6px&quot;, textAlign: &quot;center&quot; }}&gt;
							&lt;div style={{ fontSize: &quot;24px&quot;, fontWeight: &quot;bold&quot;, color: &quot;#28a745&quot; }}&gt;
								{dashboardStats.memberCount}
							&lt;/div&gt;
							&lt;div style={{ color: &quot;#6c757d&quot; }}&gt;Members&lt;/div&gt;
						&lt;/div&gt;
					&lt;/div&gt;
				&lt;/div&gt;
			)}

			{/* Members Section - available to all users */}
			&lt;div className=&quot;section&quot;&gt;
				&lt;div style={{ display: &quot;flex&quot;, justifyContent: &quot;space-between&quot;, alignItems: &quot;center&quot;, marginBottom: &quot;15px&quot; }}&gt;
					&lt;h3&gt;Team Members&lt;/h3&gt;
				&lt;/div&gt;

				{members.length === 0 ? (
					&lt;div className=&quot;empty-state&quot;&gt;No members found&lt;/div&gt;
				) : (
					&lt;table className=&quot;data-table&quot;&gt;
						&lt;thead&gt;
							&lt;tr&gt;
								&lt;th&gt;Name&lt;/th&gt;
								&lt;th&gt;Email&lt;/th&gt;
								&lt;th&gt;Role&lt;/th&gt;
							&lt;/tr&gt;
						&lt;/thead&gt;
						&lt;tbody&gt;
							{members.map((member) =&gt; (
								&lt;tr key={member.id}&gt;
									&lt;td&gt;{member.name}&lt;/td&gt;
									&lt;td&gt;{member.email}&lt;/td&gt;
									&lt;td&gt;
										&lt;span className={`role-badge ${member.role}`}&gt;
											{member.role}
										&lt;/span&gt;
									&lt;/td&gt;
								&lt;/tr&gt;
							))}
						&lt;/tbody&gt;
					&lt;/table&gt;
				)}
			&lt;/div&gt;

		&lt;/div&gt;
	);
}
">
<input type="hidden" name="project[files][src/frontend/index.html]" value="&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;Tenant Dashboard - RivetKit&lt;/title&gt;
    &lt;style&gt;
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 20px;
            background-color: #f0f0f0;
        }
        .app-container {
            max-width: 1000px;
            margin: 0 auto;
            background-color: white;
            padding: 30px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        .header {
            text-align: center;
            margin-bottom: 30px;
            padding-bottom: 20px;
            border-bottom: 2px solid #e9ecef;
        }
        .header h1 {
            color: #333;
            margin: 0;
        }
        .header p {
            color: #666;
            margin: 10px 0;
        }
        .login-section {
            text-align: center;
            padding: 40px 20px;
        }
        .login-section h2 {
            color: #333;
            margin-bottom: 20px;
        }
        .login-section p {
            color: #666;
            margin-bottom: 30px;
        }
        .login-buttons {
            display: flex;
            justify-content: center;
            gap: 20px;
            flex-wrap: wrap;
        }
        .login-button {
            padding: 15px 30px;
            font-size: 16px;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            transition: all 0.2s;
            min-width: 200px;
        }
        .login-button.admin {
            background-color: #dc3545;
            color: white;
        }
        .login-button.admin:hover {
            background-color: #c82333;
        }
        .login-button.member {
            background-color: #007bff;
            color: white;
        }
        .login-button.member:hover {
            background-color: #0056b3;
        }
        .user-info {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 30px;
            padding: 15px;
            background-color: #f8f9fa;
            border-radius: 6px;
        }
        .user-info .user-details {
            display: flex;
            align-items: center;
            gap: 15px;
        }
        .user-badge {
            padding: 4px 12px;
            border-radius: 12px;
            font-size: 12px;
            font-weight: bold;
            text-transform: uppercase;
        }
        .user-badge.admin {
            background-color: #f8d7da;
            color: #721c24;
        }
        .user-badge.member {
            background-color: #d1ecf1;
            color: #0c5460;
        }
        .logout-button {
            padding: 8px 16px;
            background-color: #6c757d;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
        }
        .logout-button:hover {
            background-color: #5a6268;
        }
        .section {
            margin-bottom: 40px;
        }
        .section h3 {
            color: #333;
            margin-bottom: 15px;
            padding-bottom: 10px;
            border-bottom: 1px solid #e9ecef;
        }
        .data-table {
            width: 100%;
            border-collapse: collapse;
            background-color: white;
            border-radius: 6px;
            overflow: hidden;
            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
        }
        .data-table th {
            background-color: #f8f9fa;
            padding: 12px;
            text-align: left;
            font-weight: bold;
            color: #495057;
            border-bottom: 2px solid #e9ecef;
        }
        .data-table td {
            padding: 12px;
            border-bottom: 1px solid #e9ecef;
        }
        .data-table tbody tr:hover {
            background-color: #f8f9fa;
        }
        .role-badge {
            padding: 4px 8px;
            border-radius: 12px;
            font-size: 12px;
            font-weight: bold;
            text-transform: uppercase;
        }
        .role-badge.admin {
            background-color: #f8d7da;
            color: #721c24;
        }
        .role-badge.member {
            background-color: #d4edda;
            color: #155724;
        }
        .status-badge {
            padding: 4px 8px;
            border-radius: 12px;
            font-size: 12px;
            font-weight: bold;
            text-transform: uppercase;
        }
        .status-badge.paid {
            background-color: #d4edda;
            color: #155724;
        }
        .status-badge.unpaid {
            background-color: #fff3cd;
            color: #856404;
        }
        .error-message {
            padding: 15px;
            background-color: #f8d7da;
            border: 1px solid #f5c6cb;
            border-radius: 6px;
            color: #721c24;
            margin-bottom: 20px;
        }
        .error-message h4 {
            margin: 0 0 10px 0;
            color: #721c24;
        }
        .info-box {
            background-color: #e8f4f8;
            border: 1px solid #b8d4da;
            border-radius: 6px;
            padding: 15px;
            margin-bottom: 20px;
        }
        .info-box h3 {
            margin: 0 0 10px 0;
            color: #2c5aa0;
        }
        .info-box p {
            margin: 0;
            color: #555;
            line-height: 1.5;
        }
        .empty-state {
            text-align: center;
            padding: 40px 20px;
            color: #6c757d;
            font-style: italic;
        }
        .organization-header {
            background-color: #f8f9fa;
            border: 1px solid #dee2e6;
            border-radius: 6px;
            padding: 20px;
            margin-bottom: 30px;
        }
        .organization-header h2 {
            margin: 0 0 10px 0;
            color: #333;
        }
        .organization-header p {
            margin: 0;
            color: #666;
        }
        @media (max-width: 768px) {
            .login-buttons {
                flex-direction: column;
                align-items: center;
            }
            .user-info {
                flex-direction: column;
                gap: 15px;
                text-align: center;
            }
            .data-table {
                font-size: 14px;
            }
            .data-table th,
            .data-table td {
                padding: 8px;
            }
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;div id=&quot;root&quot;&gt;&lt;/div&gt;
    &lt;script type=&quot;module&quot; src=&quot;/main.tsx&quot;&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;">
<input type="hidden" name="project[files][src/frontend/main.tsx]" value="import { StrictMode } from &quot;react&quot;;
import { createRoot } from &quot;react-dom/client&quot;;
import { App } from &quot;./App&quot;;

const root = document.getElementById(&quot;root&quot;);
if (!root) throw new Error(&quot;Root element not found&quot;);

createRoot(root).render(
	&lt;StrictMode&gt;
		&lt;App /&gt;
	&lt;/StrictMode&gt;
);">
<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-tenant">
</form>
<script>document.getElementById("mainForm").submit();</script>

</body></html>