Saas-Starter-Kit/Saas-Kit-prisma

4 errors, 7 warningsmain

1 critical finding to fix first.

4 errors and 7 warnings in the paths between 14 routes and 6 tables.

Every file was read, but 1 place in them could not be parsed — listed at the end of this report.

ROUTE FINDINGS7 of 14 · 12/12 rules
ROUTES
14
TABLES
6
FILES READ
134/134
RULES RUN
12/12

11 findings

1 critical3 high7 medium

  • Criticalhigh confidenceServer Action UpdateUserSubscription (src/lib/API/Database/user/mutations.ts) can reach users without authenticating.

    Anyone, signed in or not, can write to users (email, emailVerified) — personal or secret fields.

    Data reached
    users (write) · sensitive: email, emailVerified
    Missing
    Authentication before the handler reaches data, and a query scoped to the caller.
    Confidence
    No middleware, guard or wrapper is applied to this route anywhere in src/lib/API/Database/user/mutations.ts.
    Path
    Server Action UpdateUserSubscription (src/lib/API/Database/user/mutations.ts) → users

    Prove it

    # A Server Action is a public POST endpoint. Its id ships to the browser:
    # find it in your built client chunks next to "UpdateUserSubscription".
    curl -i -X POST 'https://YOUR_APP/' \
      -H 'Next-Action: ACTION_ID' \
      -H 'content-type: text/plain;charset=UTF-8' -d '[]'
    # No cookie is sent. A 200 that runs the action means anyone can call UpdateUserSubscription.

    Fix

    "use server";
    
    export async function UpdateUserSubscription(formData: FormData) {
      // The page's own session check does not run for an action: check here.
      const user = await getCurrentUser();   // e.g. (await supabase.auth.getUser()).data.user
      if (!user) throw new Error("Unauthorized");
      // ...scope every query to user.id
    }

    Regression test

    import { UpdateUserSubscription } from "@/lib/API/Database/user/mutations";
    
    // Make your session lookup return no user for this test.
    vi.mock("@/lib/auth", () => ({ getCurrentUser: async () => null }));
    
    test("UpdateUserSubscription refuses a signed-out caller", async () => {
      await expect(UpdateUserSubscription(new FormData())).rejects.toThrow();
    });

    Intended? This cannot be declared intentional: a public write is not a design choice the check will bless. Accept it as known debt: run wyro-check --update-baseline and commit the ledger. It stays visible and CI fails only on new findings.

    auth-before-data

  • Highhigh confidenceServer Action UpdateSubscription (src/lib/API/Database/subscription/mutations.ts) can reach subscriptions without authenticating.

    An unauthenticated caller can write to subscriptions. Writes are how data gets corrupted or planted.

    Data reached
    subscriptions (write)
    Missing
    Authentication before the handler reaches data, and a query scoped to the caller.
    Confidence
    No middleware, guard or wrapper is applied to this route anywhere in src/lib/API/Database/subscription/mutations.ts.
    Path
    Server Action UpdateSubscription (src/lib/API/Database/subscription/mutations.ts) → subscriptions

    Prove it

    # A Server Action is a public POST endpoint. Its id ships to the browser:
    # find it in your built client chunks next to "UpdateSubscription".
    curl -i -X POST 'https://YOUR_APP/' \
      -H 'Next-Action: ACTION_ID' \
      -H 'content-type: text/plain;charset=UTF-8' -d '[]'
    # No cookie is sent. A 200 that runs the action means anyone can call UpdateSubscription.

    Fix

    "use server";
    
    export async function UpdateSubscription(formData: FormData) {
      // The page's own session check does not run for an action: check here.
      const user = await getCurrentUser();   // e.g. (await supabase.auth.getUser()).data.user
      if (!user) throw new Error("Unauthorized");
      // ...scope every query to user.id
    }

    Regression test

    import { UpdateSubscription } from "@/lib/API/Database/subscription/mutations";
    
    // Make your session lookup return no user for this test.
    vi.mock("@/lib/auth", () => ({ getCurrentUser: async () => null }));
    
    test("UpdateSubscription refuses a signed-out caller", async () => {
      await expect(UpdateSubscription(new FormData())).rejects.toThrow();
    });

    Intended? This cannot be declared intentional: a public write is not a design choice the check will bless. Accept it as known debt: run wyro-check --update-baseline and commit the ledger. It stays visible and CI fails only on new findings.

    auth-before-data

  • Highhigh confidenceServer Action UpdateTodo (src/lib/API/Database/todos/mutations.ts) can reach todos without authenticating.

    An unauthenticated caller can write to todos. Writes are how data gets corrupted or planted.

    Data reached
    todos (write)
    Missing
    Authentication before the handler reaches data, and a query scoped to the caller.
    Confidence
    No middleware, guard or wrapper is applied to this route anywhere in src/lib/API/Database/todos/mutations.ts.
    Path
    Server Action UpdateTodo (src/lib/API/Database/todos/mutations.ts) → todos

    Prove it

    # A Server Action is a public POST endpoint. Its id ships to the browser:
    # find it in your built client chunks next to "UpdateTodo".
    curl -i -X POST 'https://YOUR_APP/' \
      -H 'Next-Action: ACTION_ID' \
      -H 'content-type: text/plain;charset=UTF-8' -d '[]'
    # No cookie is sent. A 200 that runs the action means anyone can call UpdateTodo.

    Fix

    "use server";
    
    export async function UpdateTodo(formData: FormData) {
      // The page's own session check does not run for an action: check here.
      const user = await getCurrentUser();   // e.g. (await supabase.auth.getUser()).data.user
      if (!user) throw new Error("Unauthorized");
      // ...scope every query to user.id
    }

    Regression test

    import { UpdateTodo } from "@/lib/API/Database/todos/mutations";
    
    // Make your session lookup return no user for this test.
    vi.mock("@/lib/auth", () => ({ getCurrentUser: async () => null }));
    
    test("UpdateTodo refuses a signed-out caller", async () => {
      await expect(UpdateTodo(new FormData())).rejects.toThrow();
    });

    Intended? This cannot be declared intentional: a public write is not a design choice the check will bless. Accept it as known debt: run wyro-check --update-baseline and commit the ledger. It stays visible and CI fails only on new findings.

    auth-before-data

  • Highhigh confidenceServer Action DeleteTodo (src/lib/API/Database/todos/mutations.ts) can reach todos without authenticating.

    An unauthenticated caller can write to todos. Writes are how data gets corrupted or planted.

    Data reached
    todos (write)
    Missing
    Authentication before the handler reaches data, and a query scoped to the caller.
    Confidence
    No middleware, guard or wrapper is applied to this route anywhere in src/lib/API/Database/todos/mutations.ts.
    Path
    Server Action DeleteTodo (src/lib/API/Database/todos/mutations.ts) → todos

    Prove it

    # A Server Action is a public POST endpoint. Its id ships to the browser:
    # find it in your built client chunks next to "DeleteTodo".
    curl -i -X POST 'https://YOUR_APP/' \
      -H 'Next-Action: ACTION_ID' \
      -H 'content-type: text/plain;charset=UTF-8' -d '[]'
    # No cookie is sent. A 200 that runs the action means anyone can call DeleteTodo.

    Fix

    "use server";
    
    export async function DeleteTodo(formData: FormData) {
      // The page's own session check does not run for an action: check here.
      const user = await getCurrentUser();   // e.g. (await supabase.auth.getUser()).data.user
      if (!user) throw new Error("Unauthorized");
      // ...scope every query to user.id
    }

    Regression test

    import { DeleteTodo } from "@/lib/API/Database/todos/mutations";
    
    // Make your session lookup return no user for this test.
    vi.mock("@/lib/auth", () => ({ getCurrentUser: async () => null }));
    
    test("DeleteTodo refuses a signed-out caller", async () => {
      await expect(DeleteTodo(new FormData())).rejects.toThrow();
    });

    Intended? This cannot be declared intentional: a public write is not a design choice the check will bless. Accept it as known debt: run wyro-check --update-baseline and commit the ledger. It stays visible and CI fails only on new findings.

    auth-before-data

  • Mediumlow confidencePOST /api/chat has no validator or rate limiter attached.

    POST /api/chat accepts any body at any rate. Malformed input reaches your handler, and nothing stops a script calling it in a loop.

    Missing
    Schema validation of the request body, and a rate limit.
    Confidence
    Inline validation (zod.parse, pydantic models) and platform rate limits are not always visible to the parser. Check before acting.

    Prove it

    curl -i -X POST 'https://YOUR_API/api/chat' \
      -H 'content-type: application/json' -d '{"unexpected": {"nested": [1,2,3]}}'
    # Expect 400. A 2xx or 500 means the body is not validated.

    Fix

    const Body = z.object({ name: z.string().max(200) }).strict();
    
    const parsed = Body.safeParse(req.body);
    if (!parsed.success) return res.status(400).json(parsed.error.flatten());

    Regression test

    test("POST /api/chat rejects an unexpected body", async () => {
      const res = await request(app).post("/api/chat").send({ unexpected: true });
      expect(res.status).toBe(400);
    });

    Intended? If validation and rate limiting happen upstream (an API gateway, Vercel Firewall), turn the rule off for the repo: { "policy": { "rules": { "public-entry-guarded": "off" } } } in wyro.json.

    public-entry-guarded

  • Mediumlow confidenceServer Action UpdateSubscription (src/lib/API/Database/subscription/mutations.ts) has no validator or rate limiter attached.

    Server Action UpdateSubscription (src/lib/API/Database/subscription/mutations.ts) is a public POST endpoint: it accepts whatever arguments a caller sends, as often as they send them.

    Missing
    Schema validation of the action's arguments, and a rate limit.
    Confidence
    Inline validation (zod.parse on the form data) and platform rate limits are not always visible to the parser. Check before acting.

    Fix

    const Input = z.object({ title: z.string().min(1).max(200) });
    
    export async function UpdateSubscription(formData: FormData) {
      const parsed = Input.safeParse(Object.fromEntries(formData));
      if (!parsed.success) return { error: parsed.error.flatten() };
      // ...use parsed.data, never formData directly
    }

    Intended? If validation and rate limiting happen upstream (Vercel Firewall, a middleware), turn the rule off for the repo: { "policy": { "rules": { "public-entry-guarded": "off" } } } in wyro.json.

    public-entry-guarded

  • Mediumlow confidenceServer Action UpdateTodo (src/lib/API/Database/todos/mutations.ts) has no validator or rate limiter attached.

    Server Action UpdateTodo (src/lib/API/Database/todos/mutations.ts) is a public POST endpoint: it accepts whatever arguments a caller sends, as often as they send them.

    Missing
    Schema validation of the action's arguments, and a rate limit.
    Confidence
    Inline validation (zod.parse on the form data) and platform rate limits are not always visible to the parser. Check before acting.

    Fix

    const Input = z.object({ title: z.string().min(1).max(200) });
    
    export async function UpdateTodo(formData: FormData) {
      const parsed = Input.safeParse(Object.fromEntries(formData));
      if (!parsed.success) return { error: parsed.error.flatten() };
      // ...use parsed.data, never formData directly
    }

    Intended? If validation and rate limiting happen upstream (Vercel Firewall, a middleware), turn the rule off for the repo: { "policy": { "rules": { "public-entry-guarded": "off" } } } in wyro.json.

    public-entry-guarded

  • Mediumlow confidenceServer Action DeleteTodo (src/lib/API/Database/todos/mutations.ts) has no validator or rate limiter attached.

    Server Action DeleteTodo (src/lib/API/Database/todos/mutations.ts) is a public POST endpoint: it accepts whatever arguments a caller sends, as often as they send them.

    Missing
    Schema validation of the action's arguments, and a rate limit.
    Confidence
    Inline validation (zod.parse on the form data) and platform rate limits are not always visible to the parser. Check before acting.

    Fix

    const Input = z.object({ title: z.string().min(1).max(200) });
    
    export async function DeleteTodo(formData: FormData) {
      const parsed = Input.safeParse(Object.fromEntries(formData));
      if (!parsed.success) return { error: parsed.error.flatten() };
      // ...use parsed.data, never formData directly
    }

    Intended? If validation and rate limiting happen upstream (Vercel Firewall, a middleware), turn the rule off for the repo: { "policy": { "rules": { "public-entry-guarded": "off" } } } in wyro.json.

    public-entry-guarded

  • Mediumlow confidenceServer Action UpdateUserSubscription (src/lib/API/Database/user/mutations.ts) has no validator or rate limiter attached.

    Server Action UpdateUserSubscription (src/lib/API/Database/user/mutations.ts) is a public POST endpoint: it accepts whatever arguments a caller sends, as often as they send them.

    Missing
    Schema validation of the action's arguments, and a rate limit.
    Confidence
    Inline validation (zod.parse on the form data) and platform rate limits are not always visible to the parser. Check before acting.

    Fix

    const Input = z.object({ title: z.string().min(1).max(200) });
    
    export async function UpdateUserSubscription(formData: FormData) {
      const parsed = Input.safeParse(Object.fromEntries(formData));
      if (!parsed.success) return { error: parsed.error.flatten() };
      // ...use parsed.data, never formData directly
    }

    Intended? If validation and rate limiting happen upstream (Vercel Firewall, a middleware), turn the rule off for the repo: { "policy": { "rules": { "public-entry-guarded": "off" } } } in wyro.json.

    public-entry-guarded

  • Mediumlow confidenceServer Action RetrieveSubscription (src/lib/API/Services/stripe/customer.ts) has no validator or rate limiter attached.

    Server Action RetrieveSubscription (src/lib/API/Services/stripe/customer.ts) is a public POST endpoint: it accepts whatever arguments a caller sends, as often as they send them.

    Missing
    Schema validation of the action's arguments, and a rate limit.
    Confidence
    Inline validation (zod.parse on the form data) and platform rate limits are not always visible to the parser. Check before acting.

    Fix

    const Input = z.object({ title: z.string().min(1).max(200) });
    
    export async function RetrieveSubscription(formData: FormData) {
      const parsed = Input.safeParse(Object.fromEntries(formData));
      if (!parsed.success) return { error: parsed.error.flatten() };
      // ...use parsed.data, never formData directly
    }

    Intended? If validation and rate limiting happen upstream (Vercel Firewall, a middleware), turn the rule off for the repo: { "policy": { "rules": { "public-entry-guarded": "off" } } } in wyro.json.

    public-entry-guarded

  • Mediumlow confidenceServer Action UpdateStripeCustomerEmail (src/lib/API/Services/stripe/customer.ts) has no validator or rate limiter attached.

    Server Action UpdateStripeCustomerEmail (src/lib/API/Services/stripe/customer.ts) is a public POST endpoint: it accepts whatever arguments a caller sends, as often as they send them.

    Missing
    Schema validation of the action's arguments, and a rate limit.
    Confidence
    Inline validation (zod.parse on the form data) and platform rate limits are not always visible to the parser. Check before acting.

    Fix

    const Input = z.object({ title: z.string().min(1).max(200) });
    
    export async function UpdateStripeCustomerEmail(formData: FormData) {
      const parsed = Input.safeParse(Object.fromEntries(formData));
      if (!parsed.success) return { error: parsed.error.flatten() };
      // ...use parsed.data, never formData directly
    }

    Intended? If validation and rate limiting happen upstream (Vercel Firewall, a middleware), turn the rule off for the repo: { "policy": { "rules": { "public-entry-guarded": "off" } } } in wyro.json.

    public-entry-guarded

1 place could not be parsed, so any route or table declared there is missing from this report:

  • src/app/api/auth/[...nextauth]/route.ts — Next route file exports no recognised method handler

Free account, no card. The repository opens as an editable graph.

Add this check to the README

wyro architecture badge
[![wyro architecture](https://wyro.in/api/badge/Saas-Starter-Kit/Saas-Kit-prisma)](https://wyro.in/scan/Saas-Starter-Kit/Saas-Kit-prisma)

It updates itself whenever the repository changes and links back to this report.

What this is

Wyro reads the repository’s routes and data models and checks the paths between them: whether a route can reach a table without passing a guard, whether a datastore holding personal data is exposed to a public read, whether an endpoint that issues credentials requires the credentials it issues.

It is rule-based, not a model. The same commit produces the same result every time, and it does not guess at business rules it cannot see. A rule with nothing to look at is reported as not having run — never as a pass.

This check runs on public source through GitHub’s own API and needs no account. A free account adds the editable architecture canvas, private repositories on paid plans, and a CI gate. No card required.

Maintain this repository? You can have this report removed, and a real vulnerability is disclosed to you privately before it is published. How public reports are handled.

Saas-Starter-Kit/Saas-Kit-prisma — 11 architecture findings | Wyro