diff --git a/e2e-realtime/realtime.spec.ts b/e2e-realtime/realtime.spec.ts
index 64282b4..57e7b22 100644
--- a/e2e-realtime/realtime.spec.ts
+++ b/e2e-realtime/realtime.spec.ts
@@ -12,8 +12,9 @@ test('GM hosts a session; a player on another device sees live combat', async ({
const gmCtx = await browser.newContext();
const playerCtx = await browser.newContext();
- // GM: load the sample campaign and host a session.
+ // GM: load the sample campaign and host a session (hosting requires a signed-in GM).
const gm = await fresh(gmCtx);
+ await gm.evaluate(() => localStorage.setItem('ttrpg-cloud-user', 'GM'));
gm.on('dialog', (d) => d.dismiss()); // dismiss the optional-password prompt
await gm.getByLabel('Settings').click();
await gm.getByRole('button', { name: 'Load sample campaign' }).click();
diff --git a/e2e-realtime/seats.spec.ts b/e2e-realtime/seats.spec.ts
index e89e787..498742c 100644
--- a/e2e-realtime/seats.spec.ts
+++ b/e2e-realtime/seats.spec.ts
@@ -12,8 +12,9 @@ test('a player claims a seat and manages their character; edits round-trip to th
const gmCtx = await browser.newContext();
const playerCtx = await browser.newContext();
- // GM: sample campaign + host.
+ // GM: sample campaign + host (hosting requires a signed-in GM).
const gm = await fresh(gmCtx);
+ await gm.evaluate(() => localStorage.setItem('ttrpg-cloud-user', 'GM'));
gm.on('dialog', (d) => d.dismiss()); // optional-password prompt
await gm.getByLabel('Settings').click();
await gm.getByRole('button', { name: 'Load sample campaign' }).click();
diff --git a/src/features/play/SessionControl.tsx b/src/features/play/SessionControl.tsx
index d5feed1..095bdf1 100644
--- a/src/features/play/SessionControl.tsx
+++ b/src/features/play/SessionControl.tsx
@@ -1,13 +1,16 @@
import { useState } from 'react';
+import { useNavigate } from '@tanstack/react-router';
import { useLiveQuery } from 'dexie-react-hooks';
import { useSessionStore, type SeatRequest } from '@/stores/sessionStore';
import { hostSession, stopSession, grantSeat } from '@/lib/sync/wsSync';
import { charactersRepo } from '@/lib/db/repositories';
+import { cloudUsername } from '@/lib/cloud/client';
import { useActiveCampaign } from '@/features/campaigns/hooks';
import { Button } from '@/components/ui/Button';
/** Header control: GM hosts a live session, shares the code, and approves seats. */
export function SessionControl() {
+ const navigate = useNavigate();
const campaign = useActiveCampaign();
const role = useSessionStore((s) => s.role);
const status = useSessionStore((s) => s.status);
@@ -20,6 +23,14 @@ export function SessionControl() {
if (role === 'off') {
if (!campaign) return null;
+ // Hosting a shared session requires the GM to be signed in to an account.
+ if (!cloudUsername()) {
+ return (
+
+ );
+ }
return (