Skip to content

Rework API Responses#41

Merged
christianhelp merged 45 commits intomainfrom
rework-api-responses
Feb 17, 2026
Merged

Rework API Responses#41
christianhelp merged 45 commits intomainfrom
rework-api-responses

Conversation

@christianhelp
Copy link
Contributor

@christianhelp christianhelp commented Feb 16, 2026

Why

Currently, all of our responses only return a message which can be either an error code, or some sort of data object.

What

The purpose of this PR is to rework this to be more inuitive. For success messages, we now return a data key that we return. This can be a string, but is usually some sort of db object that we return after a successful call. For errors, we will return a human-readable message key that the frontend can display to users and a code key that will mainly be used to help the frontend determine how it should behave based on what it was passed. For example, if the server returns ALREADY_MEMBER, then we should likely just display the error message and provide them an option to redirect to the team itself.

Satisfies

#37

Summary by CodeRabbit

  • New Features
    • Added password recovery (forgot-password), expanded user profiles (first/last name, last seen), and a configurable theme system.
  • Improvements
    • Standardized API error responses with descriptive codes and messages.
    • Improved team/admin membership and invite/join flows with clearer status reporting.
    • Better handling of user initials and team list rendering in the UI.
  • Other
    • Added server-side logging and structured responses for consistency.

@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 2026

Warning

Rate limit exceeded

@christianhelp has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 11 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Adds permission helpers and a structured logging subsystem to the API, standardizes error response shapes and codes across routes/middleware, extends auth/theme configuration, and adjusts client-side data access to use the new response shapes.

Changes

Cohort / File(s) Summary
Database utilities & logging
apps/api/src/lib/functions/database.ts
Adds permission/team helpers (isSiteAdminUser, leaveTeam, getAdminUserForTeam, isUserSiteAdminOrQueryHasPermissions), structured logging helpers (logError, logInfo, logWarning, logToDb, getAllContextValues), and maybeGetDbErrorCode.
Middleware
apps/api/src/lib/functions/middleware.ts
Switches some logging calls to non-awaited calls and standardizes unauthorized response payload to { message, code } using API_ERROR_MESSAGES constants.
Log routes
apps/api/src/routes/log.ts
Standardizes responses to { data } on success and { message, code } on errors; enforces site-admin checks on /admin/all; updates /:teamId to return structured 401/403 errors and data-wrapped successes.
Team routes
apps/api/src/routes/team.ts
Massive response-shape and error-code standardization: replaces string errors with { message, code }, wraps successes in { data }, tightens invite/join flows (validation, transactional join, explicit codes), refines delete/update/remove flows and permission checks.
User routes
apps/api/src/routes/user.ts
Standardizes authenticated and admin responses to { data } and { message, code }; moves admin route to /admin/:userId and enforces site-admin check with 403+code on denial.
Shared constants & config
packages/shared/constants.ts
Adds API_ERROR_MESSAGES error codes, introduces THEME_CONFIG, extends AUTH_CONFIG.additionalFields (firstName, lastName, lastSeen, siteRole), adds /forgot-password to PUBLIC_ROUTES, removes GREETINGS_FUNCTIONS and STANDARD_NANOID_SIZE.
Global error handler
apps/api/src/index.ts
Updates global 500 handler to return { message, code: API_ERROR_MESSAGES.GENERIC_ERROR } instead of raw error.
Client UI
apps/web/src/components/shared/Navbar/UserButton.tsx
Switches reading of teams from userTeamsResult?.message to userTeamsResult?.data to match new response shape.
Utilities
apps/web/src/lib/utils.ts
Improves getInitials splitting logic to trim and split on whitespace sequences.
DB schema comment
packages/db/schema.ts
Adds a TODO comment above backupJob about future team join requests (non-functional).

Sequence Diagram(s)

sequenceDiagram
  rect rgba(200,220,255,0.5)
    participant Client
  end
  rect rgba(200,255,200,0.5)
    participant API
  end
  rect rgba(255,220,200,0.5)
    participant Database
  end

  Client->>API: POST /log or action triggering permission check
  API->>API: isUserSiteAdminOrQueryHasPermissions(siteRole, query)
  API->>Database: run permission query / team lookup
  Database-->>API: query result
  API-->>API: decide allow/deny
  alt allowed
    API->>Database: logToDb(logType, message, context)
    Database-->>API: persist log
    API-->>Client: 200 { data: ... }
  else denied
    API-->>Client: 401/403 { message, code }
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • mjanderson1227

Poem

🐰
I hopped through code with tiny feet so spry,
Logging crumbs and roles now stack up high,
Responses wrapped in gentle fields, errors named,
Teams and admins checked — no chaos left untamed,
A carrot for the merge, now watch this bunny fly!

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main
Title check ✅ Passed The title 'Rework API Responses' clearly and concisely summarizes the main change: restructuring API response formats across multiple endpoints to use consistent data/message/code fields.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch rework-api-responses

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/web/src/routes/team/join.tsx (1)

4-7: ⚠️ Potential issue | 🟡 Minor

Stale comment — still references joining via team ID.

The comment mentions joining "through the team ID," but teamId is no longer extracted from search params. Update the comment to reflect the current behavior.

Suggested fix
-/*
- * Team Join Route
- * This route will allow users to join a team via an invite code or through the team ID. Team ID is only allowed if the team does not require an invite code.
- */
+/*
+ * Team Join Route
+ * This route will allow users to join a team via an invite code.
+ */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/routes/team/join.tsx` around lines 4 - 7, Update the top block
comment for the "Team Join Route" to remove the stale reference to joining
"through the team ID" and state the current behavior (joining via an invite code
only); mention that teamId is no longer extracted from search params and that
team IDs are not accepted when a team requires an invite code, ensuring the
comment near the Team Join Route header accurately reflects the code.
apps/web/package.json (1)

47-47: ⚠️ Potential issue | 🟡 Minor

Same zod version concern as in packages/shared/package.json^3.25.67 looks unusually high.

This appears in both packages/shared/package.json and here. If this version doesn't exist, installs will fail.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/package.json` at line 47, The package.json entry for the "zod"
dependency uses an unusually high version string ("zod": "^3.25.67") that may
not exist; update the "zod" dependency in apps/web/package.json (and mirror the
change in packages/shared/package.json) to a valid published version (or to the
repo's canonical zod version) so npm/yarn installs succeed, ensuring both files
use the same, existing semver value.
apps/web/src/styles.css (1)

92-92: ⚠️ Potential issue | 🟠 Major

--destructive-foreground is referenced but never defined.

Line 92 maps --color-destructive-foreground: var(--destructive-foreground), but neither :root nor .dark defines --destructive-foreground. This will result in the variable resolving to nothing, likely causing invisible or missing text on destructive elements.

Add the definition to both :root and .dark. Typically this is a white or near-white color:

 	--destructive: oklch(0.577 0.245 27.325);
+	--destructive-foreground: oklch(0.985 0 0);

And similarly in .dark.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/styles.css` at line 92, The CSS references
--destructive-foreground via --color-destructive-foreground but
--destructive-foreground is not defined; add a concrete value for
--destructive-foreground in both :root and .dark so the fallback isn't empty
(set a light color like near-white for :root and an appropriate contrasting
light color for .dark), ensuring the variables --destructive-foreground and
--color-destructive-foreground are defined and consistent with other color
tokens.
🤖 Fix all issues with AI agents
Verify each finding against the current code and only fix it if needed.


In `@apps/api/src/index.ts`:
- Around line 41-51: The global onError handler currently returns { error:
"Internal Server Error" } which is inconsistent with the API contract; update
the handler to import API_ERROR_MESSAGES and return the standard shape—e.g.,
c.json({ message: API_ERROR_MESSAGES.INTERNAL_SERVER_ERROR, code: 500 },
500)—keeping the existing logError(err.message, c) call; ensure you add the
import for API_ERROR_MESSAGES from "shared" and modify the c.json payload inside
the onError callback accordingly.
- Line 22: Replace the empty interface Env with a type alias: change the
declaration of Env from an empty interface to a type alias representing the same
empty-object type (i.e., use a type alias named Env instead of interface Env),
updating the declaration in apps/api/src/index.ts and leaving all existing
references to Env unchanged.

In `@apps/api/src/lib/auth.ts`:
- Around line 36-40: The cast "as { [key: string]: FieldAttribute<FieldType> } |
undefined" on AUTH_CONFIG.additionalFields bypasses compile-time validation;
instead, remove that runtime assertion and strongly type additionalFields where
it is declared (in packages/shared/constants.ts) using "satisfies Record<string,
FieldAttribute<FieldType>>" (and drop the optional/| undefined since
additionalFields is always defined). Then update usages (e.g., in auth.ts where
AUTH_CONFIG.additionalFields is spread) to rely on the correctly typed property
without the "as" cast so TypeScript will catch malformed field definitions at
compile time.

In `@apps/api/src/lib/functions/database.ts`:
- Around line 41-45: The isSiteAdminUser function currently uses
Array.prototype.some to check membership; replace that with a simpler
Array.prototype.includes check for
["ADMIN","SUPER_ADMIN"].includes(permissionEnum) inside isSiteAdminUser to
improve readability and intent; update the function body in the isSiteAdminUser
declaration (referencing the UserType["siteRole"] parameter) accordingly.
- Around line 94-113: The logToDb function currently swallows db.insert errors
(caught in the try/catch) and only calls console.error, which is the sole
production signal; update the catch in logToDb to emit a structured fallback so
monitoring can pick it up — include the loggingType, message, options, and the
caught error in a single JSON-like console.error/console.log line, and also call
your metrics/telemetry helper (e.g., incrementLoggingFailureCounter or
emitMetric) so failures are counted; locate the catch around db.insert in
logToDb (and use isInDevMode for the early return) and augment it to both output
the structured stdout log and increment a metric/telemetry counter.
- Around line 66-77: The function isUserSiteAdminOrQueryHasPermissions currently
accepts a Promise<T> or a () => Promise<T>, which allows callers to pass an
already-started Promise that will run even when isSiteAdminUser(userSiteRole)
short-circuits; change the API to accept only a lazy thunk (() => Promise<T>) by
removing the Promise<T> overload in isUserSiteAdminOrQueryHasPermissions so the
query is only executed when needed, update all callers to pass a function that
returns the Promise, and update the function signature and any tests/docs
referencing it to reflect the new () => Promise<T> type.

In `@apps/api/src/lib/functions/index.ts`:
- Around line 18-21: The TODO referencing issue `#38` is ambiguous; update the
comment above isInDevMode to document exactly what "proper value" refers to
(e.g., whether NODE_ENV is the intended source, or if Hono/Cloudflare Workers'
runtime env like c.env should be used) and include guidance for future changes
(why NODE_ENV === "development" is used now and when to consult issue `#38`); keep
the isInDevMode function as-is but replace the TODO with a clear explanatory
sentence that points to the issue number and the specific concern (Hono/c.env vs
process.env) so future maintainers don't need to chase the issue to understand
the intent.

In `@apps/api/src/lib/functions/middleware.ts`:
- Around line 19-22: The middleware currently awaits logInfo which synchronously
triggers logToDb on every request and adds DB write latency; change this to a
non-blocking fire-and-forget and filter out noisy routes: in the middleware
function (where logInfo is called for `c.req.path` / `userString`) skip logging
early for known noisy paths (e.g., "/health", auth callbacks) and otherwise call
logInfo without awaiting (e.g., call it and attach a .catch to swallow/log
errors) so DB writes don't block the request path; ensure logInfo/logToDb still
surface errors via the catch handler instead of blocking.
- Around line 46-48: Replace the inconsistent error shape returned in the
middleware when auth fails: keep the logInfo call, but change the c.json
response to follow the new contract by returning { message:
API_ERROR_MESSAGES.NOT_AUTHORIZED, code: 'NOT_AUTHORIZED' } with the 401 status
(update the c.json call in middleware.ts where API_ERROR_MESSAGES.NOT_AUTHORIZED
is used).

In `@apps/api/src/routes/log.ts`:
- Around line 39-40: Response payloads return query results under "message"
instead of the required "data" key; update both responses to use "data". Replace
the c.json({ message: allLogs }, 200) and the other c.json({ message: ... },
200) usages in this file (look for the log.findMany() result stored as allLogs
and the other log query return) so they return c.json({ data: allLogs }, 200)
and c.json({ data: <otherResult> }, 200) respectively; ensure no other handlers
in this file still use "message".
- Around line 71-74: The log route is returning all matching rows via
db.query.log.findMany which can blow up memory for busy teams; modify the
handler that calls db.query.log.findMany to accept and validate pagination
params (e.g., page/limit or cursor/limit) from the request, pass them to the DB
call using take/limit and skip/offset or a cursor-based where clause, order by a
deterministic column (e.g., createdAt DESC), and return paginated metadata
(total/count or nextCursor) alongside the logs instead of returning the entire
dataset.

In `@apps/api/src/routes/team.ts`:
- Around line 493-498: This response returns only { message:
API_ERROR_MESSAGES.NOT_AUTHORIZED } instead of the new API contract { message,
code } and the message is a machine code instead of human text; update the
branch that calls c.json(...) (the else branch shown) to return an object with
both a human-readable message string and a code field (use
API_ERROR_MESSAGES.NOT_AUTHORIZED for the code and a matching human message like
"Not authorized" or the existing human messages used elsewhere), and keep the
403 status; ensure consistency with other error responses in this file (same
shape and keys).
- Around line 266-274: Handlers returning a NOT_AUTHORIZED error are using HTTP
401 (unauthenticated) but should return 403 (forbidden); update the response
status code in the relevant blocks to 403. Specifically, in the DELETE handler
(where canUserDelete is checked and c.json({ message..., code:
API_ERROR_MESSAGES.NOT_AUTHORIZED }, 401) is returned) and the similar
authorization checks in the GET /:teamId/admin, GET /:teamId/members, and PATCH
/:teamId/update handlers, change the numeric status from 401 to 403 while
keeping the same message and API_ERROR_MESSAGES.NOT_AUTHORIZED constant and
using the same c.json(...) call sites.

In `@apps/api/src/routes/upload.ts`:
- Line 1: The file contains only an unused import HonoBetterAuth and no exports
or routes; remove the dead import if the upload route isn't implemented, or if
it's a placeholder add a minimal export/TODO and a stub route handler (e.g., a
router or exported function named uploadRoute/uploadHandler) so the file is
purposeful; reference HonoBetterAuth to locate the unused import and update
apps/api/src/routes/upload.ts accordingly.

In `@apps/api/src/routes/user.ts`:
- Line 23: The inline comment "This needs a permission check..." is now
stale—remove or update it to reflect that the admin authorization logic is
already implemented in this route; specifically, delete or replace the comment
near the top of user.ts so it no longer suggests missing checks and reference
the existing admin authorization middleware/logic (the code that enforces admin
role in the same file) when updating the comment.

In `@apps/web/package.json`:
- Line 47: The package.json entry for the "zod" dependency uses an unusually
high version string ("zod": "^3.25.67") that may not exist; update the "zod"
dependency in apps/web/package.json (and mirror the change in
packages/shared/package.json) to a valid published version (or to the repo's
canonical zod version) so npm/yarn installs succeed, ensuring both files use the
same, existing semver value.

In `@apps/web/src/components/shared/LeaveTeamDialog.tsx`:
- Around line 31-56: The AlertDialogTrigger being used with asChild wrapping
DropdownMenuItem causes the dropdown to close before the AlertDialog can open;
modify LeaveTeamDialog to control the AlertDialog open state manually: add state
(e.g., isOpen) for the AlertDialog and replace the current AlertDialogTrigger
asChild pattern by using DropdownMenuItem's onSelect handler to call
e.preventDefault() and set isOpen = true, and pass isOpen and an onOpenChange
handler to AlertDialog (or call the AlertDialog's open prop setter) so the
dialog opens reliably; adjust AlertDialogAction/Cancel to close via the
onOpenChange/setIsOpen handler.
- Around line 15-16: The import alias for leaveTeamMutationClient is a
concatenation typo —
`_unusedForNowleaveTeamMutationClientleaveTeamMutationClient` — and should be
cleaned up; update the import in LeaveTeamDialog.tsx to use a single clear
unused alias (e.g., `_unusedForNowLeaveTeamMutationClient`) or remove the
aliased import if truly unused, ensuring the original symbol
`leaveTeamMutationClient` and the `useMutation` alias `_unusedForNowuseMutation`
are corrected/consistent.

In `@apps/web/src/components/shared/Navbar/navbar.tsx`:
- Around line 57-63: The Login button in Navbar currently uses a raw <a
href="/sign-in"> which causes a full page reload; replace it with the
client-side router <Link to="/sign-in"> wrapping the Button (matching the Get
Started link) and remove the commented-out <Link> lines, or if the <a> was
intentional due to an auth redirect issue, add a brief comment in navbar.tsx
explaining why the raw anchor is required; update the Login Button code path
(Navbar, Login button) accordingly to ensure consistent client-side navigation.
- Around line 54-55: SignedOutNavList renders ThemeSwitcher directly inside a
div causing inconsistent layout with SignedInNavList; wrap the ThemeSwitcher
component in a <NavigationMenuItem> (same as in SignedInNavList) so both
SignedOutNavList and SignedInNavList use NavigationMenuItem to host
ThemeSwitcher, updating the JSX in navbar.tsx where SignedOutNavList,
SignedInNavList, ThemeSwitcher, and NavigationMenuItem are defined.

In `@apps/web/src/components/shared/Navbar/UserButton.tsx`:
- Line 34: In UserButton (userTeamsResult handling) the code reads
userTeamsResult?.message but the /team API returns successful payloads on the
data property; update the reference to use userTeamsResult?.data (or safely
coerce to an array, e.g., userTeams = userTeamsResult?.data ?? []) so the teams
render correctly; adjust any related type annotations or null checks around
userTeamsResult and userTeams to reflect the data property change.
- Around line 135-147: The logout handler calls invalidate() but does not await
it before calling navigate(), risking navigation with stale cache; update the
onClick async handler (the function that calls authClient.signOut(), toast,
invalidate(), and navigate()) to await invalidate() (and optionally wrap it in
try/catch or handle errors) before calling navigate({ to: "/" }) so the query
cache is fully cleared prior to routing.

In `@apps/web/src/components/ui/alert-dialog.tsx`:
- Around line 128-150: Both AlertDialogAction and AlertDialogCancel are missing
the data-slot attribute used elsewhere for styling hooks; update the JSX
returned by the AlertDialogPrimitive.Action in function AlertDialogAction and
AlertDialogPrimitive.Cancel in function AlertDialogCancel to include an
appropriate data-slot value (e.g., data-slot="action" and data-slot="cancel" or
matching existing naming convention), preserving existing className and
{...props} behavior so the attribute is applied alongside
cn(buttonVariants(...), className).

In `@apps/web/src/lib/hooks/useTheme.ts`:
- Around line 19-33: The updater passed to setTheme in switchTheme contains DOM
manipulation and localStorage writes (side effects) which is an anti-pattern;
compute the new theme value first (e.g., const newTheme = theme ===
THEME_CONFIG.light ? THEME_CONFIG.dark : THEME_CONFIG.light or derive from the
currentTheme value), then perform document.body.classList.remove/ add and
localStorage.setItem using THEME_CONFIG.accessKey, and finally call
setTheme(newTheme); ensure switchTheme no longer performs side effects inside
the setTheme functional updater.
- Around line 7-16: The code in useEffect (useTheme) reads storedTheme from
localStorage and blindly adds it to document.body; validate storedTheme against
allowed values (e.g., THEME_CONFIG.light and THEME_CONFIG.dark) before modifying
classes and calling setTheme. Change the logic in the useEffect that handles
storedTheme: if storedTheme === THEME_CONFIG.light or storedTheme ===
THEME_CONFIG.dark then remove both theme classes, add the storedTheme class and
call setTheme(storedTheme); otherwise ignore the value (or fall back to a
default) and do not add the untrusted class to document.body.

In `@apps/web/src/lib/queries.ts`:
- Around line 34-39: The current code calls apiClient.team.$get() and throws a
generic Error on non-200 responses; instead, read and parse the response body
(await response.json()) when response?.status !== 200, extract the
server-provided message and code, and throw an Error that includes that message
and attach the code (e.g., set error.message = body.message and error.code =
body.code or throw a custom error containing both) so callers of
apiClient.team.$get can display the server message or branch on the error code.

In `@apps/web/src/lib/utils.ts`:
- Around line 20-25: The getInitials function fails for empty or whitespace-only
fullName because split(" ") yields [""] and the code accesses names[0][0]
without checking; fix by trimming and splitting on whitespace, then filtering
out empty segments (e.g., use fullName.trim() and split by /\s+/ or filter out
""), then if the resulting parts array is empty return ""; otherwise safely
access parts[0][0] and parts[parts.length-1][0] (guarding for single-part names)
and .toUpperCase()—apply this change inside getInitials so it never dereferences
undefined.

In `@apps/web/src/routes/__root.tsx`:
- Line 9: The import for ErrorComponent uses incorrect casing and will fail on
case-sensitive filesystems; update the import statement that brings in
ErrorComponent (the line importing "Error") to match the actual filename
"error.tsx" (e.g., import ErrorComponent from "@/components/shared/error") so
the module name casing matches the file on disk and resolves correctly.
- Around line 29-37: The TanStackRouterDevtools component is always imported and
rendered inside the route component, adding weight to production builds; change
the component to only load and render devtools in development by conditionally
importing or lazy-loading TanStackRouterDevtools (e.g., using a dynamic import
or React.lazy inside the route component function) and gate rendering behind a
dev flag (like process.env.NODE_ENV === 'development' or import.meta.env.DEV),
keeping Providers, Navbar, and Outlet unchanged; update references to
TanStackRouterDevtools in this component so production bundles do not include
it.

In `@apps/web/src/routes/team/join.tsx`:
- Around line 4-7: Update the top block comment for the "Team Join Route" to
remove the stale reference to joining "through the team ID" and state the
current behavior (joining via an invite code only); mention that teamId is no
longer extracted from search params and that team IDs are not accepted when a
team requires an invite code, ensuring the comment near the Team Join Route
header accurately reflects the code.

In `@apps/web/src/styles.css`:
- Line 92: The CSS references --destructive-foreground via
--color-destructive-foreground but --destructive-foreground is not defined; add
a concrete value for --destructive-foreground in both :root and .dark so the
fallback isn't empty (set a light color like near-white for :root and an
appropriate contrasting light color for .dark), ensuring the variables
--destructive-foreground and --color-destructive-foreground are defined and
consistent with other color tokens.

In `@packages/db/drizzle/0005_glorious_dragon_lord.sql`:
- Around line 1-9: Add a DB-level uniqueness constraint to the team_join_request
table to prevent duplicate requests: create a UNIQUE INDEX on (team_id, user_id)
(e.g. idx_team_join_request_team_user) or, if you only want to prevent multiple
PENDING requests, create a partial/filtered UNIQUE INDEX on (team_id, user_id)
WHERE status = 'PENDING' so the database enforces the invariant instead of
relying solely on application logic.

In `@packages/db/drizzle/0007_tiny_nick_fury.sql`:
- Around line 2-10: The occurred_at column in table __new_log uses DEFAULT
(current_timestamp) which yields a text datetime but the schema expects integer
timestamps (occuredAt / integer({ mode: "timestamp_ms" })); change the default
to produce a numeric epoch value (for milliseconds use DEFAULT (unixepoch('now')
* 1000) or for seconds use DEFAULT (strftime('%s','now'))) so that the
__new_log.occurred_at column stores integer epoch timestamps consistent with the
schema.

In `@packages/db/drizzle/0008_abandoned_shiva.sql`:
- Line 1: Add an index for faster lookups on the newly added request_id column:
update the ALTER TABLE `log` migration that adds `request_id` to also create an
index (e.g., ALTER TABLE `log` ADD INDEX idx_log_request_id(request_id(255));)
or, if you can change the column type, convert `request_id` from TEXT to
VARCHAR(...) and add a normal index (ALTER TABLE `log` MODIFY request_id
VARCHAR(255), ADD INDEX idx_log_request_id(request_id)); ensure the index name
(idx_log_request_id) and index prefix length are used consistently so queries
filtering/grouping by request_id benefit.

In `@packages/db/drizzle/0009_brainy_power_man.sql`:
- Line 15: PRAGMA foreign_keys=ON is re-enabled too early; move the PRAGMA line
so FK enforcement is only turned back on after all DROP/CREATE operations for
team_invite, team_join_request, and user_to_team complete (i.e., after the table
recreations ending around line 77) to avoid DROP/INSERT failures or unintended
cascades; ensure the migration runs on LibSQL-compatible DBs if relying on this
PRAGMA behavior.
- Around line 46-47: The migration adds a FOREIGN KEY from team_invite.email →
user(email) but the Drizzle schema for the team_invite table does not declare
that relationship; either remove the FK from the SQL migration or update the
Drizzle table definition for team_invite to add a .references(...) on the email
column pointing to the user table's email (e.g., add .references(() =>
user.email) / the equivalent in your schema builder for the team_invite.email
column) so ORM and DB constraints match and behavior is explicit.

In `@packages/db/schema.ts`:
- Around line 127-137: Add DRIZZLE relation metadata for the new teamJoinRequest
table: create and export teamJoinRequestRelations that maps teamJoinRequest.team
-> many(team) and teamJoinRequest.user -> many(user) (or the reverse relations
as Drizzle expects), and update existing userRelations and teamRelations to
include many(teamJoinRequest) so queries like
db.query.teamJoinRequest.findMany({ with: { team: true, user: true } }) and
navigation from user/team to join requests work; modify the relation objects
named teamJoinRequestRelations, userRelations, and teamRelations to reference
the teamJoinRequest table identifier and appropriate relation helpers (e.g.,
many(...)/one(...)) consistent with the other relation definitions in this file.
- Around line 129-134: Remove the redundant .notNull() calls on the teamId and
userId column definitions since standardVarcharFactory() already returns a
not-null column; update the declarations for teamId and userId (the lines that
call standardVarcharFactory() and .references(() => team.id, { onDelete:
"cascade" }) / .references(() => user.id, { onDelete: "cascade" })) to omit the
extra .notNull() and leave the reference and onDelete behavior intact.

In `@packages/db/types.ts`:
- Around line 3-4: The exported types currently force nullability by defining
UserType = typeof user.$inferSelect | null and SessionType = typeof
session.$inferSelect | null; change this so consumers get a non-null base type
and an explicit nullable alias: export a non-nullable base (e.g., User = typeof
user.$inferSelect and Session = typeof session.$inferSelect) and then export
nullable variants (e.g., UserType = User | null and SessionType = Session |
null), or alternatively export both UserType and NonNullableUserType (and
likewise for Session) so callers can opt into nullability without needing
NonNullable<> elsewhere (refer to the symbols user.$inferSelect,
session.$inferSelect, UserType, SessionType).

In `@packages/shared/constants.ts`:
- Around line 67-76: API_ERROR_MESSAGES is intended as a set of string literal
constants; change its declaration to be a readonly literal by appending "as
const" to the exported object so TypeScript narrows each value to its literal
type (e.g., "NO_INVITE_CODE") instead of string. Locate the exported symbol
API_ERROR_MESSAGES and modify its definition to append as const, then run type
checks and update any code that relied on a mutable type (if necessary) to
accept readonly literal types or derive a union type from typeof
API_ERROR_MESSAGES when needed.
🟡 Minor comments (9)
packages/db/drizzle/0010_salty_romulus.sql-17-21 (1)

17-21: ⚠️ Potential issue | 🟡 Minor

Unnecessary index drop/recreate creates a brief uniqueness gap.

session_token_unique and user_email_unique are dropped and recreated identically, with no structural change to session or user. This is likely a Drizzle ORM generation artifact. Because --> statement-breakpoint separators cause each statement to run individually, there's a window where uniqueness constraints aren't enforced.

Low risk if migrations run during maintenance windows, but worth being aware of. If this is auto-generated by Drizzle, no action needed — just ensure migrations aren't run under concurrent write traffic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/db/drizzle/0010_salty_romulus.sql` around lines 17 - 21, This
migration drops and recreates the same unique indexes (session_token_unique,
user_email_unique) which creates a transient window without uniqueness due to
the statement-breakpoint separators; fix by removing the DROP and corresponding
CREATE for those two indexes (or make the migration idempotent by
checking/index-existence logic) so the migration does not drop existing
session_token_unique and user_email_unique, and keep only any true schema
changes (or convert to a single atomic ALTER/CREATE-if-not-exists flow) to avoid
the uniqueness gap.
apps/api/src/routes/backup.ts-13-21 (1)

13-21: ⚠️ Potential issue | 🟡 Minor

Successful response uses message instead of the new data key convention.

Per the PR objectives, successful responses should return a data key, while message is reserved for error responses. This endpoint returns { message: ... } on success (HTTP 200), which contradicts the stated API response rework.

Proposed fix
 		return c.json(
 			{
-				message: `Backup endpoint hit for backup ID: ${backupId}`,
+				data: `Backup endpoint hit for backup ID: ${backupId}`,
 			},
 			200,
 		);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/backup.ts` around lines 13 - 21, The handler currently
returns a successful response using the reserved "message" key; change the
response to use the "data" key instead. In the async handler (the anonymous
function receiving c, using backupId) update the c.json call to return an object
like { data: { backupId, info: `Backup endpoint hit for backup ID: ${backupId}`
} } with the same 200 status so successful responses follow the new data-key
convention and "message" remains reserved for errors.
apps/web/src/components/shared/LeaveTeamDialog.tsx-18-30 (1)

18-30: ⚠️ Potential issue | 🟡 Minor

role is declared as a required prop but is never used.

The role: string property in the component's type (line 26) forces callers to provide it, but it's not destructured or referenced. If it's reserved for future owner-handling logic (per the TODO on lines 27–29), consider making it optional or removing it until needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/shared/LeaveTeamDialog.tsx` around lines 18 - 30, The
LeaveTeamDialog component declares a required prop "role" in its props type but
never uses or destructures it; either remove "role: string" from the props type
or make it optional (role?: string) if you'll use it later, and if you intend to
reference it in future owner-handling logic, destructure role from the function
signature (e.g., { teamId: ..., teamName, isPrivate, role }) and use it where
needed. Update the props annotation accordingly in the LeaveTeamDialog
declaration to avoid forcing callers to supply an unused prop.
apps/api/src/lib/functions/database.ts-115-126 (1)

115-126: ⚠️ Potential issue | 🟡 Minor

Inconsistent null handling for context values needs standardization.

userId on line 122 uses || null, but teamId (line 123) and requestId (line 124) don't. When c.get() returns undefined for these keys, they remain undefined instead of explicitly null. Since all four fields in the log table are nullable (defined with standardVarcharFactoryNullable()), they should handle missing values consistently. Drizzle insert operations benefit from explicit null values rather than undefined.

Proposed fix
 	return {
 		route: c.req.path,
 		userId: user?.id || null,
-		teamId: c.get("teamId"),
-		requestId: c.get("requestId"),
+		teamId: c.get("teamId") ?? null,
+		requestId: c.get("requestId") ?? null,
 	};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/lib/functions/database.ts` around lines 115 - 126, The
getAllContextValues function returns mixed undefined/null for log fields; make
missing values explicit nulls to match userId and the nullable DB columns.
Update teamId and requestId (obtained via c.get("teamId") and
c.get("requestId")) to coalesce to null (e.g., use ?? null or || null) so
LoggingOptions always contains null instead of undefined for absent values; keep
route and userId handling as-is and ensure the function still returns the same
shape.
apps/api/src/routes/user.ts-21-21 (1)

21-21: ⚠️ Potential issue | 🟡 Minor

Inconsistent success response shape between routes.

The PR objective establishes that successful responses should return data under a data key. GET / correctly returns { data: user } on Line 21, but GET /admin/:userId returns { user: requestedUser } on Line 57. This inconsistency will force the frontend to handle two different response shapes for user data.

🔧 Proposed fix
-			return c.json({ user: requestedUser }, 200);
+			return c.json({ data: requestedUser }, 200);

Also applies to: 57-57

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/user.ts` at line 21, The admin route handler that
currently returns { user: requestedUser } must be made consistent with the GET /
handler that returns { data: user }; update the GET /admin/:userId response to
return { data: requestedUser } (and adjust any other places in this file
returning { user: ... } to use the data key) so all successful user responses
use the same shape; locate the response in the user route handler for GET
"/admin/:userId" and change the c.json payload accordingly.
apps/api/src/routes/log.ts-62-69 (1)

62-69: ⚠️ Potential issue | 🟡 Minor

Incorrect error message — copy-paste from the /admin/all route.

The message "Only site admins can access all logs" is misleading for the /:teamId endpoint, where team admins should also have access. This is a user-facing string per the PR's design.

Proposed fix
 			return c.json(
 				{
-					message:
-						"You are not authorized to access this endpoint. Only site admins can access all logs.",
+					message: "You are not authorized to view this team's logs.",
 					code: API_ERROR_MESSAGES.NOT_AUTHORIZED,
 				},
 				403,
 			);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/log.ts` around lines 62 - 69, Update the user-facing
authorization message returned by the team-level logs handler (the `/:teamId`
route) so it no longer says "Only site admins can access all logs"; instead
return a message that correctly reflects allowed roles (e.g., "You are not
authorized to access this endpoint. Only site admins or team admins can access
these logs.") when calling `c.json` with `API_ERROR_MESSAGES.NOT_AUTHORIZED`,
ensuring the change is applied in the handler that currently returns that
response.
apps/api/src/routes/team.ts-30-30 (1)

30-30: ⚠️ Potential issue | 🟡 Minor

Unused import: te (Telugu locale) from date-fns/locale.

This appears to be an accidental import — te is not referenced anywhere in this file.

Proposed fix
-import { te } from "date-fns/locale";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/team.ts` at line 30, Remove the accidental unused import
"te" from "date-fns/locale" in the team route file; delete the line importing te
so that the top of apps/api/src/routes/team.ts no longer includes `import { te }
from "date-fns/locale"` (and run lint/type-check to confirm no other references
remain).
apps/api/src/lib/functions/middleware.ts-56-61 (1)

56-61: ⚠️ Potential issue | 🟡 Minor

Remove console.log debug statements.

These debug console.log calls should not ship. Even though the function is a TODO placeholder, the logs leak internal context variable names and will add noise in production. Consider removing them or replacing with proper structured logging calls when the feature is completed.

Proposed fix
 export async function afterRouteLogicMiddleware(c: ApiContext, next: Next) {
 	// TODO(https://github.com/acmutsa/Fallback/issues/26): Come back and finish logging function
-	console.log("context before is: ", c.get("teamId"));
 	await next();
-	console.log("context after is: ", c.get("teamId"));
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/lib/functions/middleware.ts` around lines 56 - 61, Remove the
stray debug console.log calls from the afterRouteLogicMiddleware function:
delete both console.log("context before is: ", c.get("teamId")) and
console.log("context after is: ", c.get("teamId")). If structured logging is
required later, replace them with the app's logger (e.g., processLogger.debug or
similar) and avoid logging raw internal keys like c.get("teamId"); otherwise
leave the function body with just awaiting next() to prevent leaking internal
context.
apps/web/src/lib/queries.ts-43-43 (1)

43-43: ⚠️ Potential issue | 🟡 Minor

Inconsistent casing: joinTeamMutationclient vs leaveTeamMutationClient.

joinTeamMutationclient (lowercase c in "client") doesn't match leaveTeamMutationClient (uppercase C). This will be confusing at call sites.

Proposed fix
-export const joinTeamMutationclient = (inviteCode: string) =>
+export const joinTeamMutationClient = (inviteCode: string) =>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/lib/queries.ts` at line 43, The export name
joinTeamMutationclient has inconsistent casing compared to
leaveTeamMutationClient; rename the function/export to joinTeamMutationClient
(uppercase "C") and update any references/imports/usages to that symbol so
callers and exports match consistently (search for joinTeamMutationclient and
replace with joinTeamMutationClient in queries.ts and all call sites).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
apps/api/src/routes/log.ts (1)

25-25: Success response uses message instead of data.

For consistency with the new API contract, this should use { data: ... }. Even for acknowledgment-style responses, keeping a uniform shape makes frontend consumption predictable.

Proposed fix
-		return c.json({ message: "Log endpoint hit" }, 200);
+		return c.json({ data: "Log endpoint hit" }, 200);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/log.ts` at line 25, The success response in the log route
uses an object with a "message" key but the API contract expects a uniform "{
data: ... }" shape; update the handler's response (the c.json call that
currently returns { message: "Log endpoint hit" }, 200) to instead return {
data: "Log endpoint hit" } with the same status code so the endpoint aligns with
the new API contract.
apps/api/src/routes/team.ts (1)

162-165: logWarning / logError calls are not awaited — unhandled rejections possible.

These async logging calls are fire-and-forget. If the DB write inside them throws, you'll get unhandled promise rejections. Either await them or explicitly swallow the rejection (e.g., .catch(() => {})).

Proposed fix (example for line 162; apply similarly to lines 174 and 192)
-			logWarning(
+			await logWarning(
 				`User with ID ${user.id} is already a member of team with ID ${inviteRequest.teamId}. Transaction has been rolled back.`,
 				c,
 			);

Also applies to: 174-177, 192-195

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/team.ts` around lines 162 - 165, The async logging calls
(logWarning and logError) in the team route are being invoked fire-and-forget
which can cause unhandled promise rejections; update each call to either await
the promise (e.g., await logWarning(...)) or explicitly handle rejections (e.g.,
logWarning(...).catch(() => {})) for every occurrence referenced (logWarning at
the user membership check and the other logWarning/logError calls around
inviteRequest.teamId and context variable c) so that any thrown errors from
logWarning/logError are properly awaited or swallowed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/routes/log.ts`:
- Around line 62-69: The error message for the team-scoped logs endpoint is
incorrect; update the JSON error returned in the c.json call (the block that
returns { message: "...", code: API_ERROR_MESSAGES.NOT_AUTHORIZED } with status
403) to reflect team-scoped authorization (e.g., indicate that only site admins
or team admins for the specified team can access these logs) so it matches the
actual authorization logic for the /:teamId route.
- Around line 29-37: Split the combined check into two branches so
unauthenticated requests return 401 and non-admin authenticated users return
403: first check if user is falsy and call c.json with { message: "You are not
authenticated...", code: API_ERROR_MESSAGES.NOT_AUTHENTICATED } and status 401,
then separately check if !isSiteAdminUser(user.siteRole) and return the existing
NOT_AUTHORIZED 403 response; update the handler where variables user,
isSiteAdminUser, API_ERROR_MESSAGES and c.json are used (the auth block in log
route) to implement these two distinct responses.

In `@apps/api/src/routes/team.ts`:
- Line 48: The success response returns the payload under "message" instead of
the required "data" key; update the JSON response in the route that returns
userTeams so it uses c.json({ data: userTeams }, 200) rather than { message: ...
} — locate the response call using c.json and the userTeams variable in the
handler in team.ts and replace the key so it matches other endpoints (e.g.,
those at lines handling similar responses).
- Around line 51-81: Remove the duplicate .get("/admin") route handler (the
earlier one that returns 401/NOT_AUTHENTICATED and { message: allTeams }) so the
valid handler remains; keep only the corrected .get("/admin", async (c) => { ...
}) that uses 403/NOT_AUTHORIZED for non-admins and returns { data: allTeams }
after db.query.team.findMany(); this eliminates the dead code and ensures
correct authz semantics for the admin route.

---

Duplicate comments:
In `@apps/api/src/routes/team.ts`:
- Around line 281-289: The authorization failure branch that checks
canUserDelete returns HTTP 401 but should return 403; locate the c.json(...)
response in the canUserDelete branch in team.ts and change the HTTP status code
argument from 401 to 403 so the API_ERROR_MESSAGES.NOT_AUTHORIZED maps to HTTP
403 (forbidden) consistent with other checks in this file (e.g., nearby
authorization branches at lines around the other checks).

---

Nitpick comments:
In `@apps/api/src/routes/log.ts`:
- Line 25: The success response in the log route uses an object with a "message"
key but the API contract expects a uniform "{ data: ... }" shape; update the
handler's response (the c.json call that currently returns { message: "Log
endpoint hit" }, 200) to instead return { data: "Log endpoint hit" } with the
same status code so the endpoint aligns with the new API contract.

In `@apps/api/src/routes/team.ts`:
- Around line 162-165: The async logging calls (logWarning and logError) in the
team route are being invoked fire-and-forget which can cause unhandled promise
rejections; update each call to either await the promise (e.g., await
logWarning(...)) or explicitly handle rejections (e.g., logWarning(...).catch(()
=> {})) for every occurrence referenced (logWarning at the user membership check
and the other logWarning/logError calls around inviteRequest.teamId and context
variable c) so that any thrown errors from logWarning/logError are properly
awaited or swallowed.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/api/src/routes/log.ts (1)

25-25: ⚠️ Potential issue | 🟡 Minor

POST success response uses message instead of data.

The POST / handler returns { message: "Log endpoint hit" } on success. Per the new API contract, successful responses should use the data key. While this is just a confirmation string, aligning it with the convention keeps the contract predictable for consumers.

Proposed fix
-		return c.json({ message: "Log endpoint hit" }, 200);
+		return c.json({ data: "Log endpoint hit" }, 200);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/log.ts` at line 25, Update the POST '/' handler in the
log route so the success payload uses the API contract's "data" key instead of
"message": replace the response call that currently returns c.json({ message:
"Log endpoint hit" }, 200) with c.json({ data: "Log endpoint hit" }, 200)
(keeping the same status code and text), ensuring any tests or consumers
expecting the previous shape are updated accordingly.
🧹 Nitpick comments (2)
apps/api/src/routes/team.ts (2)

469-502: Permission check runs even for self-removal — unnecessary DB query.

isUserSiteAdminOrQueryHasPermissions (line 473) always executes getAdminUserForTeam(user.id, teamId) eagerly (the Promise is created before being passed). For self-removal (userIdToRemove === user.id), the admin check is irrelevant. Consider short-circuiting before the permission query:

Proposed fix
+		// Users can remove themselves at any time.
+		const isUserAttemptingToRemoveThemselves =
+			userIdToRemove === user.id;
+
+		if (!isUserAttemptingToRemoveThemselves) {
+			const canUserRemove = await isUserSiteAdminOrQueryHasPermissions(
+				user.siteRole,
+				getAdminUserForTeam(user.id, teamId),
+			);
+			if (!canUserRemove) {
+				return c.json(
+					{
+						message:
+							"You cannot remove this user from the team. Please contact your administrator if you believe this is an error.",
+						code: API_ERROR_MESSAGES.NOT_AUTHORIZED,
+					},
+					403,
+				);
+			}
+		}
+
+		const teamIdUserRemovedFrom = await leaveTeam(
+			userIdToRemove,
+			teamId,
+		);
+		if (teamIdUserRemovedFrom.length === 0) {
+			return c.json(
+				{
+					message: "Team or user not found.",
+					code: API_ERROR_MESSAGES.NOT_FOUND,
+				},
+				404,
+			);
+		}
+		return c.json({ data: teamIdUserRemovedFrom[0] }, 200);
-		// Users can remove themselves at any time.
-		const isUserAttemptingToRemoveThemselves =
-			userIdToRemove === user.id;
-
-		const canUserRemove = await isUserSiteAdminOrQueryHasPermissions(
-			user.siteRole,
-			getAdminUserForTeam(user.id, teamId),
-		);
-
-		if (isUserAttemptingToRemoveThemselves || canUserRemove) {
-			const teamIdUserRemovedFrom = await leaveTeam(
-				userIdToRemove,
-				teamId,
-			);
-			if (teamIdUserRemovedFrom.length === 0) {
-				return c.json(
-					{
-						message: "Team or user not found.",
-						code: API_ERROR_MESSAGES.NOT_FOUND,
-					},
-					404,
-				);
-			}
-			return c.json({ data: teamIdUserRemovedFrom[0] }, 200);
-		} else {
-			return c.json(
-				{
-					message:
-						"You cannot remove this user from the team. Please contact your administrator if you believe this is an error.",
-					code: API_ERROR_MESSAGES.NOT_AUTHORIZED,
-				},
-				403,
-			);
-		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/team.ts` around lines 469 - 502, The permission check
eagerly constructs getAdminUserForTeam even when the user is removing themself;
change the flow to short-circuit: first compute
isUserAttemptingToRemoveThemselves (userIdToRemove === user.id) and if true skip
calling isUserSiteAdminOrQueryHasPermissions/getAdminUserForTeam and proceed to
leaveTeam; only if false call getAdminUserForTeam(user.id, teamId) and await
isUserSiteAdminOrQueryHasPermissions(user.siteRole, <result of
getAdminUserForTeam>) to determine canUserRemove before continuing.

147-150: logWarning is called without await — fire-and-forget may lose logs on edge runtimes.

On Cloudflare Workers, the execution context may terminate after the response is sent. Since logWarning (line 147) and logError (line 159) are not awaited, they could be silently dropped if the runtime shuts down before the I/O completes. This also applies to logError on line 177. The logError on line 159 is awaited implicitly only because it precedes return, but logWarning at line 147 is not.

Consider awaiting these calls or using c.executionCtx.waitUntil(...) to ensure they complete.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/team.ts` around lines 147 - 150, The call to logWarning
(and other logging calls such as logError) are invoked fire-and-forget and may
be dropped on edge runtimes; update the code paths in the invite-handling
function to ensure logging completes by either awaiting the promise returned by
logWarning/logError or by passing the promise to c.executionCtx.waitUntil(...).
Specifically, replace the un-awaited logWarning invocation (and the other
non-awaited logging calls in the same function) with awaited calls or wrap them
with c.executionCtx.waitUntil(logWarning(...)) so the runtime will complete the
I/O before shutting down.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/routes/team.ts`:
- Around line 51-65: The GET "/admin" route conflates auth and authz: change the
handler in team.ts so it first checks if user exists and returns a 401 with
API_ERROR_MESSAGES.NOT_AUTHENTICATED when missing, then separately checks
isSiteAdminUser(user.siteRole) and returns a 403 with
API_ERROR_MESSAGES.NOT_AUTHORIZED (and an appropriate "Not authorized" message)
for authenticated non-admins; only call db.query.team.findMany() and return 200
when the user is present and isSiteAdminUser passes.
- Line 66: The comment "Retrieve all of the teams" above the POST '/join' route
is stale; remove or replace it to match the handler's purpose. Locate the
router.post('/join', ...) handler in team.ts (the POST '/join' endpoint) and
either delete the outdated comment or change it to a concise, accurate
description (e.g., "Handle team join requests" or "Join a team") so comments
match the /join route and handler logic.

---

Outside diff comments:
In `@apps/api/src/routes/log.ts`:
- Line 25: Update the POST '/' handler in the log route so the success payload
uses the API contract's "data" key instead of "message": replace the response
call that currently returns c.json({ message: "Log endpoint hit" }, 200) with
c.json({ data: "Log endpoint hit" }, 200) (keeping the same status code and
text), ensuring any tests or consumers expecting the previous shape are updated
accordingly.

---

Duplicate comments:
In `@apps/api/src/routes/log.ts`:
- Around line 29-37: Split the combined authentication/authorization check in
the admin-all logs route so unauthenticated users get a 401 and unauthorized
users get a 403: first check if (!user) and return c.json({message: ..., code:
API_ERROR_MESSAGES.NOT_AUTHENTICATED}, 401); else if
(!isSiteAdminUser(user.siteRole)) return c.json({message: ..., code:
API_ERROR_MESSAGES.NOT_AUTHORIZED}, 403). Follow the same pattern used in the
'/:teamId' handler and use the existing isSiteAdminUser and API_ERROR_MESSAGES
constants to locate and implement the change.

In `@apps/api/src/routes/team.ts`:
- Around line 266-274: The response for an authorized-but-not-permitted case
uses API_ERROR_MESSAGES.NOT_AUTHORIZED but incorrectly returns HTTP 401; update
the handler that checks canUserDelete (the block that calls c.json with
API_ERROR_MESSAGES.NOT_AUTHORIZED) to return status 403 instead of 401 so that
the route correctly signals "forbidden" when a user is authenticated but lacks
permission.

---

Nitpick comments:
In `@apps/api/src/routes/team.ts`:
- Around line 469-502: The permission check eagerly constructs
getAdminUserForTeam even when the user is removing themself; change the flow to
short-circuit: first compute isUserAttemptingToRemoveThemselves (userIdToRemove
=== user.id) and if true skip calling
isUserSiteAdminOrQueryHasPermissions/getAdminUserForTeam and proceed to
leaveTeam; only if false call getAdminUserForTeam(user.id, teamId) and await
isUserSiteAdminOrQueryHasPermissions(user.siteRole, <result of
getAdminUserForTeam>) to determine canUserRemove before continuing.
- Around line 147-150: The call to logWarning (and other logging calls such as
logError) are invoked fire-and-forget and may be dropped on edge runtimes;
update the code paths in the invite-handling function to ensure logging
completes by either awaiting the promise returned by logWarning/logError or by
passing the promise to c.executionCtx.waitUntil(...). Specifically, replace the
un-awaited logWarning invocation (and the other non-awaited logging calls in the
same function) with awaited calls or wrap them with
c.executionCtx.waitUntil(logWarning(...)) so the runtime will complete the I/O
before shutting down.

@christianhelp christianhelp changed the title Rework api responses Rework API Responses Feb 17, 2026
@christianhelp christianhelp merged commit 8b35c69 into main Feb 17, 2026
2 checks passed
@christianhelp christianhelp deleted the rework-api-responses branch February 17, 2026 01:25
@christianhelp christianhelp self-assigned this Feb 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant