From c10c6b836ea8be863ed978fbdbecb4054d19a1e9 Mon Sep 17 00:00:00 2001
From: Grayson Adams <51373669+GraysonCAdams@users.noreply.github.com>
Date: Sun, 8 Mar 2026 22:24:25 -0500
Subject: [PATCH 1/3] refactor: convert join flow from API endpoint to
SvelteKit page
Replace the GET-based +server.ts redirect with a proper +page.server.ts
load function and +page.svelte UI. Users now see the group name and tap
"Join Group" via a form action instead of being silently redirected.
Uses a unique placeholder phone to avoid violating the unique constraint
when multiple users are in pre-onboarding state.
---
src/routes/join/[code]/+page.server.ts | 59 +++++++
src/routes/join/[code]/+page.svelte | 228 +++++++++++++++++++++++++
src/routes/join/[code]/+server.ts | 33 ----
3 files changed, 287 insertions(+), 33 deletions(-)
create mode 100644 src/routes/join/[code]/+page.server.ts
create mode 100644 src/routes/join/[code]/+page.svelte
delete mode 100644 src/routes/join/[code]/+server.ts
diff --git a/src/routes/join/[code]/+page.server.ts b/src/routes/join/[code]/+page.server.ts
new file mode 100644
index 0000000..16ee474
--- /dev/null
+++ b/src/routes/join/[code]/+page.server.ts
@@ -0,0 +1,59 @@
+import { redirect, fail } from '@sveltejs/kit';
+import type { PageServerLoad, Actions } from './$types';
+import { validateInviteCode, createSessionToken } from '$lib/server/auth';
+import { db } from '$lib/server/db';
+import { users, notificationPreferences } from '$lib/server/db/schema';
+import { v4 as uuid } from 'uuid';
+import { checkRateLimit } from '$lib/server/rate-limit';
+
+export const load: PageServerLoad = async ({ params }) => {
+ const group = await validateInviteCode(params.code);
+ if (!group) {
+ redirect(302, '/join');
+ }
+
+ return {
+ groupName: group.name,
+ inviteCode: params.code
+ };
+};
+
+export const actions: Actions = {
+ default: async ({ params, cookies, getClientAddress }) => {
+ const ip = getClientAddress();
+ const result = checkRateLimit(`join:${ip}`, { windowMs: 15 * 60 * 1000, maxRequests: 5 });
+ if (!result.allowed) {
+ return fail(429, { error: 'Too many attempts. Please try again later.' });
+ }
+
+ const group = await validateInviteCode(params.code);
+ if (!group) {
+ redirect(302, '/join');
+ }
+
+ const userId = uuid();
+ // Use a unique placeholder phone so the unique constraint isn't violated
+ // by multiple pre-onboarding users. Onboarding replaces this with the real phone.
+ const placeholderPhone = `pending:${userId}`;
+ await db.insert(users).values({
+ id: userId,
+ username: '',
+ phone: placeholderPhone,
+ groupId: group.id,
+ createdAt: new Date()
+ });
+
+ await db.insert(notificationPreferences).values({ userId });
+
+ const token = createSessionToken(userId);
+ cookies.set('scrolly_session', token, {
+ httpOnly: true,
+ secure: true,
+ sameSite: 'lax',
+ path: '/',
+ maxAge: 60 * 60 * 24 * 365 * 10
+ });
+
+ redirect(302, '/onboard');
+ }
+};
diff --git a/src/routes/join/[code]/+page.svelte b/src/routes/join/[code]/+page.svelte
new file mode 100644
index 0000000..779218b
--- /dev/null
+++ b/src/routes/join/[code]/+page.svelte
@@ -0,0 +1,228 @@
+
+
+
+ Join {data.groupName} — scrolly
+
+
+
+
+
+
+
+
+
+
+
+
{@html iconSvg}
+
scrolly
+
your crew's private feed
+
+
+
+
+
+
+
+
+
diff --git a/src/routes/join/[code]/+server.ts b/src/routes/join/[code]/+server.ts
deleted file mode 100644
index 4d880cf..0000000
--- a/src/routes/join/[code]/+server.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { redirect } from '@sveltejs/kit';
-import type { RequestHandler } from './$types';
-import { validateInviteCode, createSessionCookie } from '$lib/server/auth';
-import { db } from '$lib/server/db';
-import { users, notificationPreferences } from '$lib/server/db/schema';
-import { v4 as uuid } from 'uuid';
-
-export const GET: RequestHandler = async ({ params }) => {
- const group = await validateInviteCode(params.code);
- if (!group) {
- redirect(302, '/join');
- }
-
- const userId = uuid();
- await db.insert(users).values({
- id: userId,
- username: '',
- phone: '',
- groupId: group.id,
- createdAt: new Date()
- });
-
- await db.insert(notificationPreferences).values({ userId });
-
- const cookie = createSessionCookie(userId);
- return new Response(null, {
- status: 302,
- headers: {
- Location: '/onboard',
- 'Set-Cookie': cookie
- }
- });
-};
From e01b61ae3dcf4dda1df16b608c15ea73e2c65ed4 Mon Sep 17 00:00:00 2001
From: Grayson Adams <51373669+GraysonCAdams@users.noreply.github.com>
Date: Sun, 8 Mar 2026 22:24:32 -0500
Subject: [PATCH 2/3] fix: use unique placeholder phone for pre-onboarding
users
Avoid violating the unique phone constraint when multiple users join
via invite code but haven't completed onboarding yet. Uses a
pending:{userId} placeholder that gets replaced with the real phone
during onboarding.
---
src/routes/api/auth/+server.ts | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/routes/api/auth/+server.ts b/src/routes/api/auth/+server.ts
index 9dddf4b..a3665a4 100644
--- a/src/routes/api/auth/+server.ts
+++ b/src/routes/api/auth/+server.ts
@@ -25,10 +25,13 @@ async function handleJoin(body: Record) {
if (!group) return json({ error: 'Invalid invite code' }, { status: 404 });
const userId = uuid();
+ // Use a unique placeholder phone so the unique constraint isn't violated
+ // by multiple pre-onboarding users. Onboarding replaces this with the real phone.
+ const placeholderPhone = `pending:${userId}`;
await db.insert(users).values({
id: userId,
username: '',
- phone: '',
+ phone: placeholderPhone,
groupId: group.id,
createdAt: new Date()
});
From 260040a1add91d243d416e2607e315d08d1ace27 Mon Sep 17 00:00:00 2001
From: Grayson Adams <51373669+GraysonCAdams@users.noreply.github.com>
Date: Sun, 8 Mar 2026 22:24:38 -0500
Subject: [PATCH 3/3] feat: add onboard page server guard
Redirect users to /join if they have no session, and to / if they've
already completed onboarding. Prevents accessing the onboard page in
invalid states.
---
src/routes/onboard/+page.server.ts | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
create mode 100644 src/routes/onboard/+page.server.ts
diff --git a/src/routes/onboard/+page.server.ts b/src/routes/onboard/+page.server.ts
new file mode 100644
index 0000000..d0ed69f
--- /dev/null
+++ b/src/routes/onboard/+page.server.ts
@@ -0,0 +1,24 @@
+import { redirect } from '@sveltejs/kit';
+import type { ServerLoad } from '@sveltejs/kit';
+import { getUserIdFromCookies, getUserWithGroup } from '$lib/server/auth';
+
+export const load: ServerLoad = async ({ request }) => {
+ const userId = getUserIdFromCookies(request.headers.get('cookie'));
+
+ if (!userId) {
+ // No session — they haven't joined via invite code
+ redirect(302, '/join');
+ }
+
+ const data = await getUserWithGroup(userId);
+
+ if (!data || data.user.removedAt) {
+ // Invalid or removed user
+ redirect(302, '/join');
+ }
+
+ if (data.user.username) {
+ // Already onboarded — send to feed
+ redirect(302, '/');
+ }
+};