Next.js 15.5.0 · tidepool
Here is what your application can actually do.
ways in — pages people can open, endpoints anything can call, and actions your forms trigger.
If you don't read code — the nine words this report uses
- Way in
- A door into your application: a page someone opens, an address other software calls, or a form that submits.
- Endpoint
- A door for other programs rather than people. Anything that knows the address can knock — which is why who-is-asking checks matter.
- GET · POST · DELETE
- The kinds of knock: asking to see something, sending something in, asking to remove something.
- Table
- Where one kind of your data lives — users, projects, invoices. Losing one loses that data.
- Outside service
- Someone else's system your app relies on: Stripe moves the money, Resend sends the email. You cannot fix their outages.
- Middleware
- Code that runs before every request — often the doorman that checks who is asking.
- Checks who is asking
- The moment code verifies identity or permission before acting. Its absence before a deletion is what we flag.
- Setting
- A value that lives outside the code, like an API key. The same code can behave differently on your machine and in production.
- file:line
- An address in your code, like
app/api/route.ts:12. Paste it into your AI assistant and ask.
What it can do
by ways inWhat it depends on
15 things · widest reach firstEvery table and outside service this application touches, and how much of it would notice if one changed.
See them as a map → · or in depth →
5 things worth checking
most serious firstDeletes rows from members — with no visible check on who is asking
Nothing in the code we followed establishes the identity or permissions of whoever triggered this. This project has middleware, but its matcher does NOT cover this path — so nothing is checking upstream either.
A static-analysis scan of this codebase flagged something worth checking. Where: app/api/admin/users/[id]/route.ts, line 5 Behaviour: DELETE /api/admin/users/[id] Finding: Deletes rows from `members` — with no visible check on who is asking Before this code runs, verify who is making the request and that they are allowed to do this. Add the smallest check that achieves that, matching how the rest of this codebase already does authorisation. Keep every other behaviour exactly the same, and show me the change as a diff. Important: if the protection already exists somewhere the scan could not follow, do not change anything — just show me where it lives.
Starts a Stripe checkout — this is where money is taken — with no visible check on who is asking
Nothing in the code we followed establishes the identity or permissions of whoever triggered this. This project has middleware, but its matcher does NOT cover this path — so nothing is checking upstream either.
A static-analysis scan of this codebase flagged something worth checking. Where: app/api/billing/checkout/route.ts, line 4 Behaviour: POST /api/billing/checkout Finding: Starts a Stripe checkout — this is where money is taken — with no visible check on who is asking Before this code runs, verify who is making the request and that they are allowed to do this. Add the smallest check that achieves that, matching how the rest of this codebase already does authorisation. Keep every other behaviour exactly the same, and show me the change as a diff. Important: if the protection already exists somewhere the scan could not follow, do not change anything — just show me where it lives.
Deletes rows from api_keys — with no visible check on who is asking
Nothing in the code we followed establishes the identity or permissions of whoever triggered this. This project has middleware, but its matcher does NOT cover this path — so nothing is checking upstream either.
A static-analysis scan of this codebase flagged something worth checking. Where: app/api/keys/route.ts, line 15 Behaviour: DELETE /api/keys Finding: Deletes rows from `api_keys` — with no visible check on who is asking Before this code runs, verify who is making the request and that they are allowed to do this. Add the smallest check that achieves that, matching how the rest of this codebase already does authorisation. Keep every other behaviour exactly the same, and show me the change as a diff. Important: if the protection already exists somewhere the scan could not follow, do not change anything — just show me where it lives.
Deletes rows from tasks — with no visible check on who is asking
Nothing in the code we followed establishes the identity or permissions of whoever triggered this. This project has middleware, but its matcher does NOT cover this path — so nothing is checking upstream either.
A static-analysis scan of this codebase flagged something worth checking. Where: app/api/projects/[id]/route.ts, line 13 Behaviour: DELETE /api/projects/[id] Finding: Deletes rows from `tasks` — with no visible check on who is asking Before this code runs, verify who is making the request and that they are allowed to do this. Add the smallest check that achieves that, matching how the rest of this codebase already does authorisation. Keep every other behaviour exactly the same, and show me the change as a diff. Important: if the protection already exists somewhere the scan could not follow, do not change anything — just show me where it lives.
Named "sendOnboardingEmail", but we found nothing here that would send an email
We traced 2 other things this does, so we could read it — the promised step is not among them. Either it happens further away than we followed, or it was never wired up.
A static-analysis scan of this codebase flagged something worth checking. Where: app/actions.ts, line 26 Behaviour: A form calls sendOnboardingEmail() Finding: Named "sendOnboardingEmail", but we found nothing here that would send an email The name promises something the body never does. Either implement the promised step, or rename the function so it stops promising it. Show me the change as a diff. Important: if the protection already exists somewhere the scan could not follow, do not change anything — just show me where it lives.
Every way in
7 groups · ranked by consequenceGrouped, and ranked by what a behaviour can do rather than by how big it is — a twelve-line delete endpoint outranks a four-hundred-line settings page.
API · projects 4 1 to check
API · admin 1 1 to check
API · billing 2 1 to check
API · keys 2 1 to check
Form actions 5 1 to check
API · tasks 2
Everything else (12 smaller areas) 12
Endpoint · DELETE
DELETE /api/projects/[id]
Other software asks to remove something here.
Deletes rows from tasks — with no visible check on who is asking
Nothing in the code we followed establishes the identity or permissions of whoever triggered this. This project has middleware, but its matcher does NOT cover this path — so nothing is checking upstream either.
A static-analysis scan of this codebase flagged something worth checking. Where: app/api/projects/[id]/route.ts, line 13 Behaviour: DELETE /api/projects/[id] Finding: Deletes rows from `tasks` — with no visible check on who is asking Before this code runs, verify who is making the request and that they are allowed to do this. Add the smallest check that achieves that, matching how the rest of this codebase already does authorisation. Keep every other behaviour exactly the same, and show me the change as a diff. Important: if the protection already exists somewhere the scan could not follow, do not change anything — just show me where it lives.
What it does
4 steps-
Changes something
Deletes rows from
tasksapp/api/projects/[id]/route.ts:14
app/api/projects/[id]/route.tsline 1412// GET during review and never made it down here. 13export async function DELETE(request: Request, { params }: { params: { id: string } }) { 14 await supabase.from('tasks').delete().eq('project_id', params.id); 15 await supabase.from('comments').delete().eq('project_id', params.id); 16 await supabase.from('projects').delete().eq('id', params.id); -
Changes something
Deletes rows from
commentsapp/api/projects/[id]/route.ts:15
app/api/projects/[id]/route.tsline 1513export async function DELETE(request: Request, { params }: { params: { id: string } }) { 14 await supabase.from('tasks').delete().eq('project_id', params.id); 15 await supabase.from('comments').delete().eq('project_id', params.id); 16 await supabase.from('projects').delete().eq('id', params.id); 17 return new Response(null, { status: 204 }); -
Changes something
Deletes rows from
projectsapp/api/projects/[id]/route.ts:16
app/api/projects/[id]/route.tsline 1614 await supabase.from('tasks').delete().eq('project_id', params.id); 15 await supabase.from('comments').delete().eq('project_id', params.id); 16 await supabase.from('projects').delete().eq('id', params.id); 17 return new Response(null, { status: 204 }); 18} -
Answers
Answers the request
app/api/projects/[id]/route.ts:17
app/api/projects/[id]/route.tsline 1715 await supabase.from('comments').delete().eq('project_id', params.id); 16 await supabase.from('projects').delete().eq('id', params.id); 17 return new Response(null, { status: 204 }); 18}
Endpoint · DELETE
DELETE /api/admin/users/[id]
Other software asks to remove something here.
Deletes rows from members — with no visible check on who is asking
Nothing in the code we followed establishes the identity or permissions of whoever triggered this. This project has middleware, but its matcher does NOT cover this path — so nothing is checking upstream either.
A static-analysis scan of this codebase flagged something worth checking. Where: app/api/admin/users/[id]/route.ts, line 5 Behaviour: DELETE /api/admin/users/[id] Finding: Deletes rows from `members` — with no visible check on who is asking Before this code runs, verify who is making the request and that they are allowed to do this. Add the smallest check that achieves that, matching how the rest of this codebase already does authorisation. Keep every other behaviour exactly the same, and show me the change as a diff. Important: if the protection already exists somewhere the scan could not follow, do not change anything — just show me where it lives.
What it does
3 steps-
Changes something
Deletes rows from
membersapp/api/admin/users/[id]/route.ts:6
app/api/admin/users/[id]/route.tsline 64// /admin/:path* but not /api/admin/:path*. 5export async function DELETE(request: Request, { params }: { params: { id: string } }) { 6 await supabase.from('members').delete().eq('user_id', params.id); 7 await supabase.from('users').delete().eq('id', params.id); 8 return new Response(null, { status: 204 }); -
Changes something
Deletes rows from
usersapp/api/admin/users/[id]/route.ts:7
app/api/admin/users/[id]/route.tsline 75export async function DELETE(request: Request, { params }: { params: { id: string } }) { 6 await supabase.from('members').delete().eq('user_id', params.id); 7 await supabase.from('users').delete().eq('id', params.id); 8 return new Response(null, { status: 204 }); 9} -
Answers
Answers the request
app/api/admin/users/[id]/route.ts:8
app/api/admin/users/[id]/route.tsline 86 await supabase.from('members').delete().eq('user_id', params.id); 7 await supabase.from('users').delete().eq('id', params.id); 8 return new Response(null, { status: 204 }); 9} 10
Endpoint · POST
POST /api/billing/checkout
Other software sends something in here.
Starts a Stripe checkout — this is where money is taken — with no visible check on who is asking
Nothing in the code we followed establishes the identity or permissions of whoever triggered this. This project has middleware, but its matcher does NOT cover this path — so nothing is checking upstream either.
A static-analysis scan of this codebase flagged something worth checking. Where: app/api/billing/checkout/route.ts, line 4 Behaviour: POST /api/billing/checkout Finding: Starts a Stripe checkout — this is where money is taken — with no visible check on who is asking Before this code runs, verify who is making the request and that they are allowed to do this. Add the smallest check that achieves that, matching how the rest of this codebase already does authorisation. Keep every other behaviour exactly the same, and show me the change as a diff. Important: if the protection already exists somewhere the scan could not follow, do not change anything — just show me where it lives.
What it does
4 steps-
Works something out
Works out
bodyapp/api/billing/checkout/route.ts:5
app/api/billing/checkout/route.tsline 53 4export async function POST(request: Request) { 5 const body = await request.json(); 6 7 const session = await stripe.checkout.sessions.create({ -
Changes something
Starts a Stripe checkout — this is where money is taken
app/api/billing/checkout/route.ts:7
app/api/billing/checkout/route.tsline 75const body = await request.json(); 6 7const session = await stripe.checkout.sessions.create({ 8 mode: 'subscription', 9 line_items: [{ price: PRICE_ID, quantity: 1 }], -
Changes something
Adds rows to
subscriptionsapp/api/billing/checkout/route.ts:14
app/api/billing/checkout/route.tsline 1412}); 13 14await supabase.from('subscriptions').insert({ 15 user_id: body.userId, 16 status: 'pending', -
Answers
Answers the request
app/api/billing/checkout/route.ts:19
app/api/billing/checkout/route.tsline 1917 }); 18 19 return Response.json({ url: session.url }); 20} 21
Endpoint · DELETE
DELETE /api/keys
Other software asks to remove something here.
Deletes rows from api_keys — with no visible check on who is asking
Nothing in the code we followed establishes the identity or permissions of whoever triggered this. This project has middleware, but its matcher does NOT cover this path — so nothing is checking upstream either.
A static-analysis scan of this codebase flagged something worth checking. Where: app/api/keys/route.ts, line 15 Behaviour: DELETE /api/keys Finding: Deletes rows from `api_keys` — with no visible check on who is asking Before this code runs, verify who is making the request and that they are allowed to do this. Add the smallest check that achieves that, matching how the rest of this codebase already does authorisation. Keep every other behaviour exactly the same, and show me the change as a diff. Important: if the protection already exists somewhere the scan could not follow, do not change anything — just show me where it lives.
What it does
3 steps-
Works something out
Works out
bodyapp/api/keys/route.ts:16
app/api/keys/route.tsline 1614 15export async function DELETE(request: Request) { 16 const body = await request.json(); 17 await supabase.from('api_keys').delete().eq('id', body.id); 18 return new Response(null, { status: 204 }); -
Changes something
Deletes rows from
api_keysapp/api/keys/route.ts:17
app/api/keys/route.tsline 1715export async function DELETE(request: Request) { 16 const body = await request.json(); 17 await supabase.from('api_keys').delete().eq('id', body.id); 18 return new Response(null, { status: 204 }); 19} -
Answers
Answers the request
app/api/keys/route.ts:18
app/api/keys/route.tsline 1816 const body = await request.json(); 17 await supabase.from('api_keys').delete().eq('id', body.id); 18 return new Response(null, { status: 204 }); 19} 20
Form action
A form calls deleteWorkspace()
What it does
5 steps-
Works something out
Works out
userapp/actions.ts:15
app/actions.tsline 1513 14export async function deleteWorkspace(workspaceId: string) { 15 const { user, role } = await requireMember(workspaceId); 16 if (role !== 'owner') throw new Error('Only the owner can do that'); 17 -
Stops here if this fails
Checks role !== 'owner' — otherwise it throws an error
app/actions.ts:16
app/actions.tsline 1614export async function deleteWorkspace(workspaceId: string) { 15 const { user, role } = await requireMember(workspaceId); 16 if (role !== 'owner') throw new Error('Only the owner can do that'); 17 18 await supabase.from('members').delete().eq('workspace_id', workspaceId); -
Changes something
Deletes rows from
membersapp/actions.ts:18
app/actions.tsline 1816if (role !== 'owner') throw new Error('Only the owner can do that'); 17 18await supabase.from('members').delete().eq('workspace_id', workspaceId); 19await supabase.from('projects').delete().eq('workspace_id', workspaceId); 20await supabase.from('workspaces').delete().eq('id', workspaceId); -
Changes something
Deletes rows from
projectsapp/actions.ts:19
app/actions.tsline 1917 18await supabase.from('members').delete().eq('workspace_id', workspaceId); 19await supabase.from('projects').delete().eq('workspace_id', workspaceId); 20await supabase.from('workspaces').delete().eq('id', workspaceId); 21await record('workspace.delete', user.id, workspaceId); -
Changes something
Deletes rows from
workspacesapp/actions.ts:20
app/actions.tsline 2018 await supabase.from('members').delete().eq('workspace_id', workspaceId); 19 await supabase.from('projects').delete().eq('workspace_id', workspaceId); 20 await supabase.from('workspaces').delete().eq('id', workspaceId); 21 await record('workspace.delete', user.id, workspaceId); 22}
Form action
A form calls sendOnboardingEmail()
Named "sendOnboardingEmail", but we found nothing here that would send an email
We traced 2 other things this does, so we could read it — the promised step is not among them. Either it happens further away than we followed, or it was never wired up.
A static-analysis scan of this codebase flagged something worth checking. Where: app/actions.ts, line 26 Behaviour: A form calls sendOnboardingEmail() Finding: Named "sendOnboardingEmail", but we found nothing here that would send an email The name promises something the body never does. Either implement the promised step, or rename the function so it stops promising it. Show me the change as a diff. Important: if the protection already exists somewhere the scan could not follow, do not change anything — just show me where it lives.
What it does
3 steps-
Changes something
Reads rows from
usersapp/actions.ts:27
app/actions.tsline 2725// left for later, and the name never changed. 26export async function sendOnboardingEmail(userId: string) { 27 const { data: user } = await supabase.from('users').select('email').eq('id', userId).single(); 28 await supabase.from('users').update({ onboarded_at: new Date().toISOString() }).eq('id', userId); 29 return user; -
Changes something
Changes rows in
usersapp/actions.ts:28
app/actions.tsline 2826export async function sendOnboardingEmail(userId: string) { 27 const { data: user } = await supabase.from('users').select('email').eq('id', userId).single(); 28 await supabase.from('users').update({ onboarded_at: new Date().toISOString() }).eq('id', userId); 29 return user; 30} -
Answers
Answers the request
app/actions.ts:29
app/actions.tsline 2927 const { data: user } = await supabase.from('users').select('email').eq('id', userId).single(); 28 await supabase.from('users').update({ onboarded_at: new Date().toISOString() }).eq('id', userId); 29 return user; 30} 31
Endpoint · DELETE
DELETE /api/tasks/[id]
Other software asks to remove something here.
What it does
4 steps-
Works something out
Works out
bodyapp/api/tasks/[id]/route.ts:14
app/api/tasks/[id]/route.tsline 1412 13export async function DELETE(request: Request, { params }: { params: { id: string } }) { 14 const body = await request.json(); 15 const { user } = await requireMember(body.workspaceId); 16 await supabase.from('tasks').delete().eq('id', params.id); -
Works something out
Works out
userapp/api/tasks/[id]/route.ts:15
app/api/tasks/[id]/route.tsline 1513export async function DELETE(request: Request, { params }: { params: { id: string } }) { 14 const body = await request.json(); 15 const { user } = await requireMember(body.workspaceId); 16 await supabase.from('tasks').delete().eq('id', params.id); 17 await record('task.delete', user.id, params.id); -
Changes something
Deletes rows from
tasksapp/api/tasks/[id]/route.ts:16
app/api/tasks/[id]/route.tsline 1614const body = await request.json(); 15const { user } = await requireMember(body.workspaceId); 16await supabase.from('tasks').delete().eq('id', params.id); 17await record('task.delete', user.id, params.id); 18return new Response(null, { status: 204 }); -
Answers
Answers the request
app/api/tasks/[id]/route.ts:18
app/api/tasks/[id]/route.tsline 1816 await supabase.from('tasks').delete().eq('id', params.id); 17 await record('task.delete', user.id, params.id); 18 return new Response(null, { status: 204 }); 19} 20
Endpoint · POST
POST /api/billing/portal
Other software sends something in here.
What it does
4 steps-
Changes something
Checks who is asking, via
requireUser()app/api/billing/portal/route.ts:6
app/api/billing/portal/route.tsline 64 5export async function POST() { 6 const user = await requireUser(); 7 const { data } = await supabase 8 .from('subscriptions') -
Changes something
Reads rows from
subscriptionsapp/api/billing/portal/route.ts:7
app/api/billing/portal/route.tsline 75export async function POST() { 6 const user = await requireUser(); 7 const { data } = await supabase 8 .from('subscriptions') 9 .select('customer_id') -
Changes something
Opens the Stripe billing portal, where a customer can change what they pay
app/api/billing/portal/route.ts:13
app/api/billing/portal/route.tsline 1311 .single(); 12 13const session = await stripe.billingPortal.sessions.create({ 14 customer: data!.customer_id, 15 return_url: process.env.APP_URL + '/settings', -
Answers
Answers the request
app/api/billing/portal/route.ts:18
app/api/billing/portal/route.tsline 1816 }); 17 18 return Response.json({ url: session.url }); 19} 20
Endpoint · POST
POST /api/ai/summarise
Other software sends something in here.
What it does
4 steps-
Works something out
Works out
bodyapp/api/ai/summarise/route.ts:7
app/api/ai/summarise/route.tsline 75 6export async function POST(request: Request) { 7 const body = await request.json(); 8 await requireMember(body.workspaceId); 9 -
Changes something
Reads rows from
commentsapp/api/ai/summarise/route.ts:10
app/api/ai/summarise/route.tsline 108await requireMember(body.workspaceId); 9 10const { data: comments } = await supabase 11 .from('comments') 12 .select('body') -
Changes something
Asks a language model to write something, streaming the reply — this costs money per request
This takes a different path depending on the setting
OPENAI_MODEL, so it may not behave the same in production as it does locally.app/api/ai/summarise/route.ts:15
app/api/ai/summarise/route.tsline 1513 .eq('project_id', body.projectId); 14 15const result = streamText({ 16 model: openai(process.env.OPENAI_MODEL ?? 'gpt-4o-mini'), 17 prompt: `Summarise this discussion:\n${(comments ?? []).map((c) => c.body).join('\n')}`, -
Answers
Answers the request
app/api/ai/summarise/route.ts:20
app/api/ai/summarise/route.tsline 2018 }); 19 20 return result.toTextStreamResponse(); 21} 22
Depends on configuration
OPENAI_MODEL, so it may not behave the same in production as it does locally.
Endpoint · GET
GET /api/export
Other software asks to see something here.
What it does
4 steps-
Works something out
Works out
workspaceIdapp/api/export/route.ts:5
app/api/export/route.tsline 53// Debug switch left in. In development it skips the ownership filter entirely. 4export async function GET(request: Request) { 5 const workspaceId = new URL(request.url).searchParams.get('workspace'); 6 7 const query = supabase.from('files').select('*'); -
Changes something
Reads rows from
filesapp/api/export/route.ts:7
app/api/export/route.tsline 75const workspaceId = new URL(request.url).searchParams.get('workspace'); 6 7const query = supabase.from('files').select('*'); 8const { data } = 9 process.env.NODE_ENV === 'development' -
Works something out
Works out
dataThis takes a different path depending on the setting
NODE_ENV, so it may not behave the same in production as it does locally.app/api/export/route.ts:8
app/api/export/route.tsline 86 7const query = supabase.from('files').select('*'); 8const { data } = 9 process.env.NODE_ENV === 'development' 10 ? await query -
Answers
Answers the request
app/api/export/route.ts:13
app/api/export/route.tsline 1311 : await query.eq('workspace_id', workspaceId); 12 13 return Response.json(data); 14} 15
Depends on configuration
NODE_ENV, so it may not behave the same in production as it does locally.
Endpoint · POST
POST /api/invite
Other software sends something in here.
What it does
5 steps-
Works something out
Works out
bodyapp/api/invite/route.ts:6
app/api/invite/route.tsline 64 5export async function POST(request: Request) { 6 const body = await request.json(); 7 const { user } = await requireMember(body.workspaceId); 8 -
Works something out
Works out
userapp/api/invite/route.ts:7
app/api/invite/route.tsline 75export async function POST(request: Request) { 6 const body = await request.json(); 7 const { user } = await requireMember(body.workspaceId); 8 9 const { data: invitation } = await supabase -
Changes something
Adds rows to
invitationsapp/api/invite/route.ts:9
app/api/invite/route.tsline 97const { user } = await requireMember(body.workspaceId); 8 9const { data: invitation } = await supabase 10 .from('invitations') 11 .insert({ email: body.email, workspace_id: body.workspaceId, invited_by: user.id }) -
Changes something
Sends an email
app/api/invite/route.ts:15
app/api/invite/route.tsline 1513 .single(); 14 15 await sendMail(body.email, 'You have been invited', 'Come and join us on Tidepool.'); 16 return Response.json(invitation); 17} -
Answers
Answers the request
app/api/invite/route.ts:16
app/api/invite/route.tsline 1614 15 await sendMail(body.email, 'You have been invited', 'Come and join us on Tidepool.'); 16 return Response.json(invitation); 17} 18
Endpoint · PATCH
PATCH /api/tasks/[id]
Other software changes something here.
What it does
4 steps-
Works something out
Works out
bodyapp/api/tasks/[id]/route.ts:6
app/api/tasks/[id]/route.tsline 64 5export async function PATCH(request: Request, { params }: { params: { id: string } }) { 6 const body = await request.json(); 7 const { user } = await requireMember(body.workspaceId); 8 await supabase.from('tasks').update({ status: body.status }).eq('id', params.id); -
Works something out
Works out
userapp/api/tasks/[id]/route.ts:7
app/api/tasks/[id]/route.tsline 75export async function PATCH(request: Request, { params }: { params: { id: string } }) { 6 const body = await request.json(); 7 const { user } = await requireMember(body.workspaceId); 8 await supabase.from('tasks').update({ status: body.status }).eq('id', params.id); 9 await record('task.update', user.id, params.id); -
Changes something
Changes rows in
tasksapp/api/tasks/[id]/route.ts:8
app/api/tasks/[id]/route.tsline 86const body = await request.json(); 7const { user } = await requireMember(body.workspaceId); 8await supabase.from('tasks').update({ status: body.status }).eq('id', params.id); 9await record('task.update', user.id, params.id); 10return new Response(null, { status: 204 }); -
Answers
Answers the request
app/api/tasks/[id]/route.ts:10
app/api/tasks/[id]/route.tsline 108 await supabase.from('tasks').update({ status: body.status }).eq('id', params.id); 9 await record('task.update', user.id, params.id); 10 return new Response(null, { status: 204 }); 11} 12
Page
Someone opens /projects/[id]
What it does
3 steps-
Changes something
Reads rows from
tasksapp/projects/[id]/page.tsx:6
app/projects/[id]/page.tsxline 64export default async function ProjectPage({ params }: { params: { id: string } }) { 5 await requireMember(params.id); 6 const { data: tasks } = await supabase.from('tasks').select('*').eq('project_id', params.id); 7 const { data: comments } = await supabase.from('comments').select('*').eq('project_id', params.id); 8 return <main><pre>{JSON.stringify({ tasks, comments })}</pre></main>; -
Changes something
Reads rows from
commentsapp/projects/[id]/page.tsx:7
app/projects/[id]/page.tsxline 75 await requireMember(params.id); 6 const { data: tasks } = await supabase.from('tasks').select('*').eq('project_id', params.id); 7 const { data: comments } = await supabase.from('comments').select('*').eq('project_id', params.id); 8 return <main><pre>{JSON.stringify({ tasks, comments })}</pre></main>; 9} -
Answers
Answers the request
app/projects/[id]/page.tsx:8
app/projects/[id]/page.tsxline 86 const { data: tasks } = await supabase.from('tasks').select('*').eq('project_id', params.id); 7 const { data: comments } = await supabase.from('comments').select('*').eq('project_id', params.id); 8 return <main><pre>{JSON.stringify({ tasks, comments })}</pre></main>; 9} 10
Form action
A form calls archiveProject()
What it does
2 steps-
Works something out
Works out
userapp/actions.ts:9
app/actions.tsline 97 8export async function archiveProject(projectId: string, workspaceId: string) { 9 const { user } = await requireMember(workspaceId); 10 await supabase.from('projects').update({ archived: true }).eq('id', projectId); 11 await record('project.archive', user.id, projectId); -
Changes something
Changes rows in
projectsapp/actions.ts:10
app/actions.tsline 108export async function archiveProject(projectId: string, workspaceId: string) { 9 const { user } = await requireMember(workspaceId); 10 await supabase.from('projects').update({ archived: true }).eq('id', projectId); 11 await record('project.archive', user.id, projectId); 12}
Form action
A form calls inviteTeammate()
What it does
3 steps-
Works something out
Works out
userapp/actions.ts:33
app/actions.tsline 3331 32export async function inviteTeammate(email: string, workspaceId: string) { 33 const { user } = await requireMember(workspaceId); 34 await supabase.from('invitations').insert({ email, workspace_id: workspaceId, invited_by: user.id }); 35 await sendMail(email, 'Join the workspace', 'You have been invited to Tidepool.'); -
Changes something
Adds rows to
invitationsapp/actions.ts:34
app/actions.tsline 3432export async function inviteTeammate(email: string, workspaceId: string) { 33 const { user } = await requireMember(workspaceId); 34 await supabase.from('invitations').insert({ email, workspace_id: workspaceId, invited_by: user.id }); 35 await sendMail(email, 'Join the workspace', 'You have been invited to Tidepool.'); 36} -
Changes something
Sends an email
app/actions.ts:35
app/actions.tsline 3533 const { user } = await requireMember(workspaceId); 34 await supabase.from('invitations').insert({ email, workspace_id: workspaceId, invited_by: user.id }); 35 await sendMail(email, 'Join the workspace', 'You have been invited to Tidepool.'); 36} 37
Endpoint · POST
POST /api/files
Other software sends something in here.
What it does
3 steps-
Works something out
Works out
bodyapp/api/files/route.ts:5
app/api/files/route.tsline 53 4export async function POST(request: Request) { 5 const body = await request.json(); 6 await requireMember(body.workspaceId); 7 const { data } = await supabase -
Changes something
Adds rows to
filesapp/api/files/route.ts:7
app/api/files/route.tsline 75const body = await request.json(); 6await requireMember(body.workspaceId); 7const { data } = await supabase 8 .from('files') 9 .insert({ name: body.name, workspace_id: body.workspaceId }) -
Answers
Answers the request
app/api/files/route.ts:12
app/api/files/route.tsline 1210 .select() 11 .single(); 12 return Response.json(data); 13} 14
Endpoint · GET
GET /api/projects/[id]
Other software asks to see something here.
What it does
3 steps-
Works something out
Works out
bodyapp/api/projects/[id]/route.ts:5
app/api/projects/[id]/route.tsline 53 4export async function GET(request: Request, { params }: { params: { id: string } }) { 5 const body = new URL(request.url).searchParams; 6 await requireMember(body.get('workspaceId')!); 7 const { data } = await supabase.from('projects').select('*').eq('id', params.id).single(); -
Changes something
Reads rows from
projectsapp/api/projects/[id]/route.ts:7
app/api/projects/[id]/route.tsline 75 const body = new URL(request.url).searchParams; 6 await requireMember(body.get('workspaceId')!); 7 const { data } = await supabase.from('projects').select('*').eq('id', params.id).single(); 8 return Response.json(data); 9} -
Answers
Answers the request
app/api/projects/[id]/route.ts:8
app/api/projects/[id]/route.tsline 86 await requireMember(body.get('workspaceId')!); 7 const { data } = await supabase.from('projects').select('*').eq('id', params.id).single(); 8 return Response.json(data); 9} 10
Page
Someone opens /settings
What it does
4 steps-
Changes something
Checks who is asking, via
requireUser()app/settings/page.tsx:5
app/settings/page.tsxline 53 4export default async function Settings() { 5 const user = await requireUser(); 6 const { data } = await supabase.from('users').select('*').eq('id', user.id).single(); 7 const { data: subscription } = await supabase -
Changes something
Reads rows from
usersapp/settings/page.tsx:6
app/settings/page.tsxline 64export default async function Settings() { 5 const user = await requireUser(); 6 const { data } = await supabase.from('users').select('*').eq('id', user.id).single(); 7 const { data: subscription } = await supabase 8 .from('subscriptions') -
Changes something
Reads rows from
subscriptionsapp/settings/page.tsx:7
app/settings/page.tsxline 75const user = await requireUser(); 6const { data } = await supabase.from('users').select('*').eq('id', user.id).single(); 7const { data: subscription } = await supabase 8 .from('subscriptions') 9 .select('*') -
Answers
Answers the request
app/settings/page.tsx:12
app/settings/page.tsxline 1210 .eq('user_id', user.id) 11 .single(); 12 return <main><pre>{JSON.stringify({ data, subscription })}</pre></main>; 13} 14
Form action
A form calls rotateApiKey()
What it does
2 steps-
Changes something
Checks who is asking, via
requireUser()app/actions.ts:39
app/actions.tsline 3937 38export async function rotateApiKey(keyId: string) { 39 const user = await requireUser(); 40 await supabase.from('api_keys').update({ secret: crypto.randomUUID() }).eq('id', keyId); 41 await record('key.rotate', user.id, keyId); -
Changes something
Changes rows in
api_keysapp/actions.ts:40
app/actions.tsline 4038export async function rotateApiKey(keyId: string) { 39 const user = await requireUser(); 40 await supabase.from('api_keys').update({ secret: crypto.randomUUID() }).eq('id', keyId); 41 await record('key.rotate', user.id, keyId); 42}
Endpoint · POST
POST /api/keys
Other software sends something in here.
What it does
4 steps-
Changes something
Checks who is asking, via
requireUser()app/api/keys/route.ts:5
app/api/keys/route.tsline 53 4export async function POST(request: Request) { 5 const user = await requireUser(); 6 const body = await request.json(); 7 const { data } = await supabase -
Works something out
Works out
bodyapp/api/keys/route.ts:6
app/api/keys/route.tsline 64export async function POST(request: Request) { 5 const user = await requireUser(); 6 const body = await request.json(); 7 const { data } = await supabase 8 .from('api_keys') -
Changes something
Adds rows to
api_keysapp/api/keys/route.ts:7
app/api/keys/route.tsline 75const user = await requireUser(); 6const body = await request.json(); 7const { data } = await supabase 8 .from('api_keys') 9 .insert({ user_id: user.id, label: body.label, scope: body.scope }) -
Answers
Answers the request
app/api/keys/route.ts:12
app/api/keys/route.tsline 1210 .select() 11 .single(); 12 return Response.json(data); 13} 14
Endpoint · GET
GET /api/projects
Other software asks to see something here.
What it does
3 steps-
Changes something
Checks who is asking, via
requireUser()app/api/projects/route.ts:5
app/api/projects/route.tsline 53 4export async function GET() { 5 const user = await requireUser(); 6 const { data } = await supabase.from('projects').select('*').eq('owner_id', user.id); 7 return Response.json(data); -
Changes something
Reads rows from
projectsapp/api/projects/route.ts:6
app/api/projects/route.tsline 64export async function GET() { 5 const user = await requireUser(); 6 const { data } = await supabase.from('projects').select('*').eq('owner_id', user.id); 7 return Response.json(data); 8} -
Answers
Answers the request
app/api/projects/route.ts:7
app/api/projects/route.tsline 75 const user = await requireUser(); 6 const { data } = await supabase.from('projects').select('*').eq('owner_id', user.id); 7 return Response.json(data); 8} 9
Endpoint · POST
POST /api/projects
Other software sends something in here.
What it does
4 steps-
Changes something
Checks who is asking, via
requireUser()app/api/projects/route.ts:11
app/api/projects/route.tsline 119 10export async function POST(request: Request) { 11 const user = await requireUser(); 12 const body = await request.json(); 13 const { data } = await supabase -
Works something out
Works out
bodyapp/api/projects/route.ts:12
app/api/projects/route.tsline 1210export async function POST(request: Request) { 11 const user = await requireUser(); 12 const body = await request.json(); 13 const { data } = await supabase 14 .from('projects') -
Changes something
Adds rows to
projectsapp/api/projects/route.ts:13
app/api/projects/route.tsline 1311const user = await requireUser(); 12const body = await request.json(); 13const { data } = await supabase 14 .from('projects') 15 .insert({ name: body.name, owner_id: user.id }) -
Answers
Answers the request
app/api/projects/route.ts:18
app/api/projects/route.tsline 1816 .select() 17 .single(); 18 return Response.json(data); 19} 20
Page
Someone opens /dashboard
What it does
3 steps-
Changes something
Checks who is asking, via
requireUser()app/dashboard/page.tsx:5
app/dashboard/page.tsxline 53 4export default async function Dashboard() { 5 const user = await requireUser(); 6 const { data: projects } = await supabase 7 .from('projects') -
Changes something
Reads rows from
projectsapp/dashboard/page.tsx:6
app/dashboard/page.tsxline 64export default async function Dashboard() { 5 const user = await requireUser(); 6 const { data: projects } = await supabase 7 .from('projects') 8 .select('*') -
Answers
Answers the request
app/dashboard/page.tsx:11
app/dashboard/page.tsxline 119 .eq('owner_id', user.id); 10 11 return <main><h1>Your projects</h1><pre>{JSON.stringify(projects)}</pre></main>; 12} 13
Endpoint · POST
POST /api/webhooks/stripe
Other software sends something in here.
What it does
5 steps-
Works something out
Works out
signatureapp/api/webhooks/stripe/route.ts:5
app/api/webhooks/stripe/route.tsline 53 4export async function POST(request: Request) { 5 const signature = request.headers.get('stripe-signature'); 6 const raw = await request.text(); 7 -
Works something out
Works out
rawapp/api/webhooks/stripe/route.ts:6
app/api/webhooks/stripe/route.tsline 64export async function POST(request: Request) { 5 const signature = request.headers.get('stripe-signature'); 6 const raw = await request.text(); 7 8 // Verifying the signature IS the authorisation here: it proves the request -
Changes something
Verifies the request really came from Stripe
app/api/webhooks/stripe/route.ts:10
app/api/webhooks/stripe/route.tsline 108// Verifying the signature IS the authorisation here: it proves the request 9// came from Stripe and not from someone who guessed the URL. 10const event = stripe.webhooks.constructEvent( 11 raw, 12 signature!, -
Changes something
Adds or changes rows in
subscriptions- Adds rows to
invoices
app/api/webhooks/stripe/route.ts:16
app/api/webhooks/stripe/route.tsline 1614); 15 16if (event.type === 'checkout.session.completed') { 17 await supabase.from('subscriptions').upsert({ status: 'active' }); 18 await supabase.from('invoices').insert({ amount: 0 }); - Adds rows to
-
Answers
Answers the request
app/api/webhooks/stripe/route.ts:21
app/api/webhooks/stripe/route.tsline 2119 } 20 21 return Response.json({ received: true }); 22} 23
Page
Someone opens /admin
What it does
3 steps-
Changes something
Reads rows from
usersapp/admin/page.tsx:5
app/admin/page.tsxline 53export default async function AdminPage() { 4 // Reached only through middleware, which does cover /admin. 5 const { data: users } = await supabase.from('users').select('*'); 6 const { data: invoices } = await supabase.from('invoices').select('*'); 7 return <main><pre>{JSON.stringify({ users, invoices })}</pre></main>; -
Changes something
Reads rows from
invoicesapp/admin/page.tsx:6
app/admin/page.tsxline 64 // Reached only through middleware, which does cover /admin. 5 const { data: users } = await supabase.from('users').select('*'); 6 const { data: invoices } = await supabase.from('invoices').select('*'); 7 return <main><pre>{JSON.stringify({ users, invoices })}</pre></main>; 8} -
Answers
Answers the request
app/admin/page.tsx:7
app/admin/page.tsxline 75 const { data: users } = await supabase.from('users').select('*'); 6 const { data: invoices } = await supabase.from('invoices').select('*'); 7 return <main><pre>{JSON.stringify({ users, invoices })}</pre></main>; 8} 9
Every request
Every request to /dashboard/:path*, /projects/:path*, /settings/:path*, /admin/:path*, before anything else
What it does
3 steps-
Works something out
Works out
tokenmiddleware.ts:5
middleware.tsline 53 4export function middleware(request: NextRequest) { 5 const token = request.cookies.get('session'); 6 if (!token) { 7 return NextResponse.redirect(new URL('/login', request.url)); -
Stops here if this fails
Checks !token — otherwise it stops here
middleware.ts:6
middleware.tsline 64export function middleware(request: NextRequest) { 5 const token = request.cookies.get('session'); 6 if (!token) { 7 return NextResponse.redirect(new URL('/login', request.url)); 8 } -
Answers
Answers the request
middleware.ts:9
middleware.tsline 97 return NextResponse.redirect(new URL('/login', request.url)); 8 } 9 return NextResponse.next(); 10} 11
Page
Someone opens the home page
What it does
1 step-
Answers
Answers the request
app/page.tsx:2
app/page.tsxline 21export default function Home() { 2 return <main><h1>Tidepool</h1><p>Somewhere for a team to keep its work.</p></main>; 3} 4
Page
Someone opens /pricing
What it does
1 step-
Answers
Answers the request
app/pricing/page.tsx:2
app/pricing/page.tsxline 21export default function Pricing() { 2 return <main><h1>Pricing</h1><p>Free while we are small.</p></main>; 3} 4
table
members
What breaks if you change it
Rename or remove this table and 11 behaviours break.
2 places can delete from it. Data removed here is gone for everything above.
What happens next
If you remove or rename members…
12 behaviours affected in total
These name it directly, so they break the moment it is gone.
and 3 more
Every behaviour that wrote to these is in the list above. They would still exist and still be readable — but nothing would be updating them any more. This is the failure with no error message.
These read the tables above. They would not crash and they would not warn you — they would carry on serving whatever was there when the writing stopped.
Only names written literally in the code are here at all. Anything reached dynamically is missing from every wave.
Everything that reaches it
11membersPOST /api/ai/summarise
Reads rows from membersPOST /api/files
Reads rows from membersPOST /api/invite
Reads rows from membersGET /api/projects/[id]
Reads rows from membersPATCH /api/tasks/[id]
Reads rows from membersDELETE /api/tasks/[id]
Reads rows from membersSomeone opens /projects/[id]
Reads rows from membersA form calls archiveProject()
Reads rows from membersA form calls deleteWorkspace()
Deletes rows from membersReads rows from membersA form calls inviteTeammate()
Reads rows from members
table
projects
What breaks if you change it
Rename or remove this table and 7 behaviours break.
2 places can delete from it. Data removed here is gone for everything above.
2 places write to it — change its shape and each of those needs updating too.
What happens next
If you remove or rename projects…
9 behaviours affected in total
These name it directly, so they break the moment it is gone.
Every behaviour that wrote to these is in the list above. They would still exist and still be readable — but nothing would be updating them any more. This is the failure with no error message.
These read the tables above. They would not crash and they would not warn you — they would carry on serving whatever was there when the writing stopped.
Only names written literally in the code are here at all. Anything reached dynamically is missing from every wave.
Everything that reaches it
7projectsPOST /api/projects
Adds rows to projectsGET /api/projects/[id]
Reads rows from projectsDELETE /api/projects/[id]
Deletes rows from projectsSomeone opens /dashboard
Reads rows from projectsA form calls archiveProject()
Changes rows in projectsA form calls deleteWorkspace()
Deletes rows from projects
table
audit_log
What breaks if you change it
Rename or remove this table and 5 behaviours break.
5 places write to it — change its shape and each of those needs updating too.
What happens next
If you remove or rename audit_log…
5 behaviours affected in total
These name it directly, so they break the moment it is gone.
Every behaviour that wrote to these is in the list above. They would still exist and still be readable — but nothing would be updating them any more. This is the failure with no error message.
Only names written literally in the code are here at all. Anything reached dynamically is missing from every wave.
table
tasks
What breaks if you change it
Rename or remove this table and 4 behaviours break.
2 places can delete from it. Data removed here is gone for everything above.
1 place writes to it — change its shape and each of those needs updating too.
What happens next
If you remove or rename tasks…
5 behaviours affected in total
These name it directly, so they break the moment it is gone.
Every behaviour that wrote to these is in the list above. They would still exist and still be readable — but nothing would be updating them any more. This is the failure with no error message.
These read the tables above. They would not crash and they would not warn you — they would carry on serving whatever was there when the writing stopped.
Only names written literally in the code are here at all. Anything reached dynamically is missing from every wave.
table
users
What breaks if you change it
Rename or remove this table and 4 behaviours break.
1 place can delete from it. Data removed here is gone for everything above.
1 place writes to it — change its shape and each of those needs updating too.
What happens next
If you remove or rename users…
4 behaviours affected in total
These name it directly, so they break the moment it is gone.
Only names written literally in the code are here at all. Anything reached dynamically is missing from every wave.
table
subscriptions
What breaks if you change it
Rename or remove this table and 4 behaviours break.
2 places write to it — change its shape and each of those needs updating too.
What happens next
If you remove or rename subscriptions…
5 behaviours affected in total
These name it directly, so they break the moment it is gone.
Every behaviour that wrote to these is in the list above. They would still exist and still be readable — but nothing would be updating them any more. This is the failure with no error message.
These read the tables above. They would not crash and they would not warn you — they would carry on serving whatever was there when the writing stopped.
Only names written literally in the code are here at all. Anything reached dynamically is missing from every wave.
table
comments
What breaks if you change it
Rename or remove this table and 3 behaviours break.
1 place can delete from it. Data removed here is gone for everything above.
What happens next
If you remove or rename comments…
3 behaviours affected in total
These name it directly, so they break the moment it is gone.
Only names written literally in the code are here at all. Anything reached dynamically is missing from every wave.
table
api_keys
What breaks if you change it
Rename or remove this table and 3 behaviours break.
1 place can delete from it. Data removed here is gone for everything above.
2 places write to it — change its shape and each of those needs updating too.
What happens next
If you remove or rename api_keys…
3 behaviours affected in total
These name it directly, so they break the moment it is gone.
Only names written literally in the code are here at all. Anything reached dynamically is missing from every wave.
table
invoices
What breaks if you change it
Rename or remove this table and 2 behaviours break.
1 place writes to it — change its shape and each of those needs updating too.
What happens next
If you remove or rename invoices…
2 behaviours affected in total
These name it directly, so they break the moment it is gone.
Only names written literally in the code are here at all. Anything reached dynamically is missing from every wave.
Everything that reaches it
2invoicesPOST /api/webhooks/stripe
Adds rows to invoices
service
Stripe
What breaks if you change it
If Stripe is unavailable or changes its API, 2 behaviours stop working.
2 places send something to it. Those calls have effects outside your application that you cannot undo.
What happens next
If Stripe stops responding…
2 behaviours affected in total
These call it directly. Whatever they were doing with it will fail.
Only names written literally in the code are here at all. Anything reached dynamically is missing from every wave.
table
files
What breaks if you change it
Rename or remove this table and 2 behaviours break.
1 place writes to it — change its shape and each of those needs updating too.
What happens next
If you remove or rename files…
2 behaviours affected in total
These name it directly, so they break the moment it is gone.
Only names written literally in the code are here at all. Anything reached dynamically is missing from every wave.
Everything that reaches it
2filesPOST /api/files
Adds rows to files
table
invitations
What breaks if you change it
Rename or remove this table and 2 behaviours break.
2 places write to it — change its shape and each of those needs updating too.
What happens next
If you remove or rename invitations…
2 behaviours affected in total
These name it directly, so they break the moment it is gone.
Only names written literally in the code are here at all. Anything reached dynamically is missing from every wave.
Everything that reaches it
2invitationsA form calls inviteTeammate()
Adds rows to invitations
service
What breaks if you change it
If email is unavailable or changes its API, 2 behaviours stop working.
2 places send something to it. Those calls have effects outside your application that you cannot undo.
What happens next
If email stops responding…
2 behaviours affected in total
These call it directly. Whatever they were doing with it will fail.
Every behaviour that wrote to these is in the list above. They would still exist and still be readable — but nothing would be updating them any more. This is the failure with no error message.
Only names written literally in the code are here at all. Anything reached dynamically is missing from every wave.
Everything that reaches it
2table
workspaces
What breaks if you change it
Rename or remove this table and 1 behaviour breaks.
1 place can delete from it. Data removed here is gone for everything above.
What happens next
If you remove or rename workspaces…
1 behaviour affected in total
These name it directly, so they break the moment it is gone.
Only names written literally in the code are here at all. Anything reached dynamically is missing from every wave.
Everything that reaches it
1workspaces
service
language model
What breaks if you change it
If language model is unavailable or changes its API, 1 behaviour stops working.
1 place sends something to it. Those calls have effects outside your application that you cannot undo.
What happens next
If language model stops responding…
1 behaviour affected in total
These call it directly. Whatever they were doing with it will fail.
Only names written literally in the code are here at all. Anything reached dynamically is missing from every wave.
Everything that reaches it
115 dependencies · 24 connections
Everything this application leans on.
Two things are joined when some behaviour reaches both — so a line means they get changed together, whether or not anyone meant them to.
15 dependencies · in depth
The same map, with room to breathe.
Drag to turn it. Nearer means larger and brighter; the clusters are groups of tables your application almost always touches together.
3 scans · 22:45:12 to 22:45:21
Your application, over time.
Move along the track to see what the system looked like at each scan, and what moved to get there.
2026-08-07 22:45:12
- Ways in
- 28
- Things it does
- 113
- Worth checking
- 3
- Moved
- 0
2026-08-07 22:45:16
- Ways in
- 28
- Things it does
- 112-1
- Worth checking
- 4+1
- Moved
- 1
Changed
A form calls sendOnboardingEmail()
2026-08-07 22:45:21
- Ways in
- 28
- Things it does
- 108-4
- Worth checking
- 5+1
- Moved
- 1
Changed
DELETE /api/projects/[id]
requireUser()membersCompared with 2026-08-07 22:45
What moved since the last scan
Changed
DELETE /api/projects/[id]
requireUser()members