Sidebar Preferences

Per-user setting for which dashboard sidebar navigation items are hidden. Stored in the shared settings table, surfaced in Settings → Display.

Architecture

  • Uses getSetting/setSetting from @/lib/db/settings-helpers.server over the shared settings pgTable (composite (user_id, key) primary key) , see architecture/postgres-port.md for the DB layer.
  • Stores a JSON array of URL path patterns (e.g. ["/dashboard/ai-chat"]) under a single settings key, serialized with JSON.stringify/JSON.parse , not a dedicated table, since this is one small blob per user rather than rows that need querying individually.
  • Input is bounded: at most 50 entries, each a /-prefixed path matching ^/[\w\-/]*$, max 200 chars , these are nav item paths, not arbitrary user content. The unbounded version previously let an authenticated user write an unbounded payload into their own settings row, which would then be loaded in full by the data-export path.
  • Default (no row): all sidebar items visible ([])
  • Dedicated query key in query-keys.ts for cache management
  • Server fn: getSidebarPreferencesFn (GET), updateSidebarPreferencesFn (POST)

Gate ordering

The write is a single-user settings blob replace , no read-then-write check on existing state, so it's a straightforward mutation inside withUserTransaction, no pre-transaction gate needed beyond the rate limiter:

export const updateSidebarPreferencesFn = createServerFn({ method: 'POST' })
  .validator(
    z.object({
      hiddenUrls: z
        .array(z.string().max(200).regex(/^\/[\w\-/]*$/))
        .max(50)
    })
  )
  .handler(async ({ data }) => {
    const user = await requireUser();
    if (!user) return err(ERROR_CODES.UNAUTHORIZED, 'Not authenticated');
    if (sidebarPreferencesLimiter.check(user.id))
      return err(ERROR_CODES.RATE_LIMITED, 'Too many requests');

    return withUserTransaction(user.id, async (tx, userId) => {
      await setSetting(
        tx,
        userId,
        HIDDEN_SIDEBAR_ITEMS_KEY,
        JSON.stringify(data.hiddenUrls)
      );
      return ok(undefined);
    });
  });

Reading (getSidebarPreferencesFn) parses the stored JSON, falling back to [] when no row exists.

Tests

Covered in src/features/sidebar-preferences/server/sidebar-preferences-crud.test.ts.