diff --git a/frontend/src/routes/(public)/signup/+page.server.ts b/frontend/src/routes/(public)/signup/+page.server.ts
new file mode 100644
index 0000000..383c855
--- /dev/null
+++ b/frontend/src/routes/(public)/signup/+page.server.ts
@@ -0,0 +1,23 @@
+import { redirect, fail } from '@sveltejs/kit';
+import { apiClient } from '$lib/server/api';
+import type { Actions } from './$types';
+
+export const actions = {
+ default: async ({ request, fetch }) => {
+ const formData = await request.formData();
+ const displayName = formData.get('displayName') as string;
+ const email = formData.get('email') as string;
+ const password = formData.get('password') as string;
+
+ const api = apiClient(fetch);
+ const { data, error } = await api.POST('/v1/auth/signup', {
+ body: { displayName, email, password }
+ });
+
+ if (error) {
+ return fail(400, { error: 'Registrierung fehlgeschlagen. Bitte versuche es erneut.' });
+ }
+
+ redirect(303, '/household/setup');
+ }
+} satisfies Actions;
diff --git a/frontend/src/routes/(public)/signup/+page.svelte b/frontend/src/routes/(public)/signup/+page.svelte
new file mode 100644
index 0000000..70027d2
--- /dev/null
+++ b/frontend/src/routes/(public)/signup/+page.svelte
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/frontend/src/routes/(public)/signup/page.server.test.ts b/frontend/src/routes/(public)/signup/page.server.test.ts
new file mode 100644
index 0000000..0a18c07
--- /dev/null
+++ b/frontend/src/routes/(public)/signup/page.server.test.ts
@@ -0,0 +1,85 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+
+vi.mock('$env/dynamic/private', () => ({
+ env: { BACKEND_URL: 'http://localhost:8080' }
+}));
+
+const mockPost = vi.fn();
+vi.mock('$lib/server/api', () => ({
+ apiClient: () => ({ POST: mockPost })
+}));
+
+describe('signup form action', () => {
+ let actions: any;
+
+ beforeEach(async () => {
+ mockPost.mockReset();
+ const mod = await import('./+page.server');
+ actions = mod.actions;
+ });
+
+ function createRequest(formData: Record) {
+ const fd = new FormData();
+ for (const [key, value] of Object.entries(formData)) {
+ fd.append(key, value);
+ }
+ return {
+ request: { formData: () => Promise.resolve(fd) },
+ fetch: vi.fn(),
+ cookies: { get: vi.fn(), set: vi.fn() }
+ } as any;
+ }
+
+ it('calls POST /v1/auth/signup with form data', async () => {
+ mockPost.mockResolvedValue({ data: { data: { id: '123' } }, error: undefined });
+
+ try {
+ await actions.default(createRequest({
+ displayName: 'Sarah',
+ email: 'sarah@example.com',
+ password: 'password123'
+ }));
+ } catch {
+ // redirect throws
+ }
+
+ expect(mockPost).toHaveBeenCalledWith('/v1/auth/signup', {
+ body: {
+ displayName: 'Sarah',
+ email: 'sarah@example.com',
+ password: 'password123'
+ }
+ });
+ });
+
+ it('redirects to /household/setup on success', async () => {
+ mockPost.mockResolvedValue({ data: { data: { id: '123' } }, error: undefined });
+
+ try {
+ await actions.default(createRequest({
+ displayName: 'Sarah',
+ email: 'sarah@example.com',
+ password: 'password123'
+ }));
+ expect.unreachable();
+ } catch (e: any) {
+ expect(e.status).toBe(303);
+ expect(e.location).toBe('/household/setup');
+ }
+ });
+
+ it('returns fail with error message on API error', async () => {
+ mockPost.mockResolvedValue({
+ data: undefined,
+ error: { status: 409, message: 'Email already registered' }
+ });
+
+ const result = await actions.default(createRequest({
+ displayName: 'Sarah',
+ email: 'sarah@example.com',
+ password: 'password123'
+ }));
+
+ expect(result.status).toBe(400);
+ });
+});