Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ Never hand-edit generated files: `packages/api-types/types/**`, `**/routeTree.ge

**Language** — Use U.S. English everywhere.

**Public surfaces** — this repo is public: PR descriptions, issues, and code comments are world-readable. Keep internal content out of them: absolute production metrics (event counts, user counts, revenue figures: state percentages, ratios, or relative change instead), internal decision detail (vendor, legal, pricing, or strategy discussions), and competitor names (protocol identifiers such as user-agent strings are fine). Put that context in the Linear issue and link it.

## Skills

The skills in `.claude/skills/` are the source of truth for conventions — load the relevant ones before working, don't guess:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/studio-e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ jobs:
filters: |
studio:
- 'packages/pg-meta/**'
- 'packages/ui/**'
- 'packages/ui-patterns/**'
- 'apps/studio/**'
- 'apps/ui-library/**'
- 'apps/design-system/**'
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/studio-unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ on:
branches: [master, studio]
paths:
- 'apps/studio/**'
- 'packages/ui/**'
- 'packages/ui-patterns/**'
- 'pnpm-lock.yaml'
pull_request:
branches: [master, studio]
Expand Down Expand Up @@ -44,6 +46,8 @@ jobs:
with:
filters: |
relevant:
- 'packages/ui/**'
- 'packages/ui-patterns/**'
- 'apps/studio/**'
- 'pnpm-lock.yaml'
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
Expand Down
14 changes: 12 additions & 2 deletions apps/docs/content/guides/functions/auth-headers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ Edge Functions care about two request headers. Sending the wrong credential in t
| `Authorization` | `Bearer <user-jwt>` | A user signed in through Supabase Auth |
| `apikey` | `sb_publishable_...` or `sb_secret_...` | Calls from clients or services |

A common mistake is sending a publishable or secret key as a bearer token: `Authorization: Bearer sb_publishable_...`. The new API keys are not JWTs. The platform check can't validate them, and your handler can't verify them as JWTs either. Instead, put API keys in the `apikey` header.
<Admonition type="note" title="Don't send API keys as bearer tokens">

A common mistake is sending a publishable or secret key as a bearer token: `Authorization: Bearer sb_publishable_...`. The new API keys are not JWTs. The platform check still accepts them, but your handler can't verify them as JWTs. Instead, put API keys in the `apikey` header.

</Admonition>

You can send both headers together. A signed-in user calling your function through `supabase-js`, for example, sends their session JWT in `Authorization` and the project's publishable key in `apikey`.

Expand All @@ -26,7 +30,13 @@ When `verify_jwt` is enabled (the default), the platform inspects the `Authoriza

The check validates legacy HS256 JWTs and JWTs signed with the new asymmetric [signing keys](/docs/guides/auth/signing-keys).

The check does not accept an API key. Publishable and secret keys are not JWTs, so callers that send one in the `Authorization` header fail the check before their request reaches your handler.
Publishable and secret keys are not JWTs, but the check still accepts them in the `Authorization` header, so callers that send one there reach your handler.

<Admonition type="caution" title="API keys and the verify_jwt check">

For migration compatibility, `verify_jwt` accepts publishable and secret keys on either header, so a key on `apikey` passes the check too. The check alone doesn't authenticate a caller that sends only an API key. Reserve `Authorization` for user tokens, and send API keys on `apikey`. To move off legacy keys entirely, migrate to the `@supabase/server` SDK as shown in [Securing Edge Functions](/docs/guides/functions/auth).

</Admonition>

Use the `verify_jwt` flag to match how the function is called:

Expand Down
4 changes: 2 additions & 2 deletions apps/studio/.claude/skills/explorer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ Compose the toolbar from slots rather than adding resource-specific props:
<ExplorerToolbarTitle>{/* static or editable title */}</ExplorerToolbarTitle>
<ExplorerToolbarActions>
{/* badges, source controls, display controls, and direct actions */}
<ExplorerToolbarAction aria-label="Run query" icon={<Play />} />
<ExplorerToolbarAction aria-label="Run query" icon={<Play size={16} strokeWidth={2} />} />
</ExplorerToolbarActions>
</ExplorerToolbar>
```

- The row defaults to 40px and follows `--header-height` at the `md` breakpoint.
- Use `ExplorerToolbarAction` for compact direct actions. Icon-only actions are 28px wide automatically.
- Use `ExplorerToolbarAction` for compact direct actions. Icon-only actions are 28px wide automatically. It defaults to `text-tertiary-foreground` and `hover:text-foreground`. Pass `size={16} strokeWidth={2}` on Lucide icons.
- Keep execution, persistence, source selection, and other resource state in the consuming Explorer surface.
- Extend layouts with children and `className`; avoid boolean props for resource-specific variants.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export const TokenDetails = ({ control, setValue }: TokenDetailsProps) => {
render={({ field }) => (
<FormItemLayout name="tokenName" label="Name" layout="flex-row-reverse">
<FormControl>
<Input id="tokenName" {...field} placeholder="e.g. CI deploy token" />
<Input {...field} placeholder="e.g. CI deploy token" />
</FormControl>
</FormItemLayout>
)}
Expand All @@ -72,11 +72,11 @@ export const TokenDetails = ({ control, setValue }: TokenDetailsProps) => {
name="expiresAt"
control={control}
render={({ field }) => (
<FormItemLayout name="expiresAt" label="Expires in" layout="flex-row-reverse">
<FormItemLayout id="expiresAt" label="Expires in" layout="flex-row-reverse">
<div className="flex gap-2 w-full">
<FormControl className="grow">
<Select value={field.value} onValueChange={handleExpiryChange}>
<SelectTrigger>
<SelectTrigger id="expiresAt">
<SelectValue placeholder="Select an expiry" />
</SelectTrigger>
<SelectContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,13 +276,11 @@ export const SessionsAuthSettingsForm = () => {
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
name="SESSIONS_SINGLE_PER_USER"
label="Enforce single session per user"
description="If enabled, all but a user's most recently active session will be terminated."
>
<FormControl>
<Switch
id="SESSIONS_SINGLE_PER_USER"
checked={field.value}
onCheckedChange={field.onChange}
disabled={!canUpdateConfig || !hasUserSessionsEntitlement}
Expand All @@ -300,14 +298,12 @@ export const SessionsAuthSettingsForm = () => {
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
name="SESSIONS_TIMEBOX"
label="Time-box user sessions"
description={`The amount of time before a user is forced to sign in again. Use 0 for never. Maximum ${MAX_SESSIONS_TIMEBOX_HOURS} hours (1 year).`}
>
<FormControl className="w-full">
<InputGroup>
<FormInputGroupInput
id="SESSIONS_TIMEBOX"
type="number"
min={0}
{...field}
Expand All @@ -332,14 +328,12 @@ export const SessionsAuthSettingsForm = () => {
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
name="SESSIONS_INACTIVITY_TIMEOUT"
label="Inactivity timeout"
description={`The amount of time a user needs to be inactive to be forced to sign in again. Use 0 for never. Maximum ${MAX_SESSIONS_INACTIVITY_TIMEOUT_HOURS} hours (1 year).`}
>
<FormControl className="w-full">
<InputGroup>
<FormInputGroupInput
id="SESSIONS_INACTIVITY_TIMEOUT"
type="number"
min={0}
{...field}
Expand Down Expand Up @@ -413,14 +407,12 @@ export const SessionsAuthSettingsForm = () => {
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
name="JWT_EXP"
label="Access token expiry time"
description="How long access tokens are valid for before they must be refreshed. Recommendation: 3600 seconds."
>
<FormControl className="w-full">
<InputGroup>
<FormInputGroupInput
id="JWT_EXP"
type="number"
min={1}
{...field}
Expand Down Expand Up @@ -480,13 +472,11 @@ export const SessionsAuthSettingsForm = () => {
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
name="REFRESH_TOKEN_ROTATION_ENABLED"
label="Detect and revoke potentially compromised refresh tokens"
description="Prevent replay attacks from potentially compromised refresh tokens."
>
<FormControl>
<Switch
id="REFRESH_TOKEN_ROTATION_ENABLED"
checked={field.value}
onCheckedChange={field.onChange}
disabled={!canUpdateConfig}
Expand All @@ -503,14 +493,12 @@ export const SessionsAuthSettingsForm = () => {
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
name="SECURITY_REFRESH_TOKEN_REUSE_INTERVAL"
label="Refresh token reuse interval"
description={`Time interval where the same refresh token can be used multiple times to request for an access token. Recommendation: 10 seconds. Maximum ${MAX_REFRESH_TOKEN_REUSE_INTERVAL_SECONDS} seconds (5 minutes).`}
>
<FormControl className="w-full">
<InputGroup>
<FormInputGroupInput
id="SECURITY_REFRESH_TOKEN_REUSE_INTERVAL"
type="number"
min={0}
{...field}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,15 @@ export const ExplorerChatToolbar = ({
<div className="z-30 sticky top-0">
<ExplorerToolbar aria-label="Chat toolbar">
<ExplorerToolbarIcon>
<MessageSquare />
<MessageSquare size={16} strokeWidth={2} />
</ExplorerToolbarIcon>
<ExplorerToolbarTitle onSaveTitle={handleSaveName}>{chat?.name ?? ''}</ExplorerToolbarTitle>
<ExplorerToolbarActions>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ExplorerToolbarAction
aria-label="More options"
icon={<MoreVertical />}
icon={<MoreVertical size={16} strokeWidth={2} />}
disabled={isChatLoading}
/>
</DropdownMenuTrigger>
Expand Down
14 changes: 7 additions & 7 deletions apps/studio/components/interfaces/Explorer/ExplorerHomeTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,20 @@ export const ExplorerHomeTab = () => {

<section className="mt-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<ActionCard
icon={<SquareCode className="h-4 w-4 text-foreground" strokeWidth={1.5} />}
title="Run SQL"
description="Write and run an ad-hoc query"
bgColor="bg-blue-500"
onClick={() => createQuery()}
/>
<ActionCard
icon={<NotebookText className="h-4 w-4 text-foreground" strokeWidth={1.5} />}
title="Create a notebook"
description="Combine notes, queries, and results"
bgColor="bg-blue-500"
onClick={() => createNotebook()}
/>
<ActionCard
icon={<SquareCode className="h-4 w-4 text-foreground" strokeWidth={1.5} />}
title="Run SQL"
description="Write and run an ad-hoc query"
bgColor="bg-blue-500"
onClick={createQuery}
/>
</div>
</section>

Expand Down
19 changes: 14 additions & 5 deletions apps/studio/components/interfaces/Explorer/ExplorerNotebookTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -359,12 +359,18 @@ export const ExplorerNotebookTab = () => {
<div className="flex flex-col h-full bg-surface-100">
<ExplorerToolbar className="px-4">
<ExplorerToolbarIcon>
<NotebookText size={14} className="text-foreground-light" />
<NotebookText size={16} strokeWidth={2} />
</ExplorerToolbarIcon>
<ExplorerToolbarTitle onSaveTitle={handleSaveTitle}>{name ?? ''}</ExplorerToolbarTitle>
<ExplorerToolbarActions>
<ExplorerToolbarAction
icon={<AiIconAnimation size={16} />}
className="group"
icon={
<AiIconAnimation
size={16}
className="text-tertiary-foreground group-hover:text-brand"
/>
}
loading={isCreating}
disabled={cells.length === 0}
tooltip={cells.length === 0 ? 'Add a cell to the notebook to analyze it' : undefined}
Expand All @@ -374,15 +380,18 @@ export const ExplorerNotebookTab = () => {
</ExplorerToolbarAction>
<ExplorerToolbarAction
aria-label="Save changes"
icon={<Save />}
icon={<Save size={16} strokeWidth={2} />}
tooltip="Save changes"
loading={isUpdating}
onClick={handleSaveNotebook}
/>
<ExplorerToolbarActions>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ExplorerToolbarAction aria-label="More options" icon={<MoreVertical />} />
<ExplorerToolbarAction
aria-label="More options"
icon={<MoreVertical size={16} strokeWidth={2} />}
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem
Expand All @@ -405,7 +414,7 @@ export const ExplorerNotebookTab = () => {
</ExplorerToolbarActions>
<ExplorerToolbarAction
aria-label="Run notebook"
icon={<Play />}
icon={<Play size={16} strokeWidth={2} />}
tooltip="Run notebook"
loading={isRunningNotebook}
disabled={queryCellIds.length === 0}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,10 @@ export const ExplorerQueryTab = () => {
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ExplorerToolbarAction icon={<Save />} tooltip="Save query" />
<ExplorerToolbarAction
icon={<Save size={16} strokeWidth={2} />}
tooltip="Save query"
/>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-52" align="end">
<DropdownMenuSub>
Expand Down Expand Up @@ -250,7 +253,7 @@ export const ExplorerQueryTab = () => {
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ExplorerToolbarAction icon={<MoreVertical />} />
<ExplorerToolbarAction icon={<MoreVertical size={16} strokeWidth={2} />} />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
Expand Down
23 changes: 17 additions & 6 deletions apps/studio/components/interfaces/Explorer/ExplorerToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const ExplorerToolbarIcon = ({
<span
data-slot="explorer-toolbar-icon"
aria-hidden={ariaHidden}
className={cn('shrink-0 text-foreground-muted [&_svg]:size-3.5', className)}
className={cn('shrink-0 text-tertiary-foreground', className)}
{...props}
/>
)
Expand Down Expand Up @@ -98,14 +98,19 @@ const ExplorerToolbarTitle = ({
}}
/>
) : onSaveTitle ? (
<Button
variant="text"
<ExplorerToolbarAction
className="group/title"
onClick={handleStartEditing}
iconRight={<Edit className="opacity-0 group-hover/title:opacity-100 transition" />}
iconRight={
<Edit
size={16}
strokeWidth={2}
className="opacity-0 transition group-hover/title:opacity-100 group-focus-visible/title:opacity-100"
/>
}
>
{title}
</Button>
</ExplorerToolbarAction>
) : (
title
)}
Expand Down Expand Up @@ -139,6 +144,7 @@ export type ExplorerToolbarActionProps = Omit<
/**
* The standard tiny, text-style button used for a direct toolbar action.
* Icon-only actions receive the prototype's compact 28px width automatically.
* `!h-auto !w-auto` lets Lucide's `size` prop win over Button `tiny`'s 14px icon box.
*/
const ExplorerToolbarAction = ({
children,
Expand All @@ -152,7 +158,12 @@ const ExplorerToolbarAction = ({
data-slot="explorer-toolbar-action"
variant="text"
size="tiny"
className={cn(children == null && 'w-7 px-0', className)}
className={cn(
'text-tertiary-foreground hover:text-foreground data-[state=open]:text-foreground',
'[&>[aria-hidden]]:text-current [&_svg]:!h-auto [&_svg]:!w-auto',
children == null && 'w-7 px-0',
className
)}
tooltip={{ content: { side: 'bottom', text: tooltip } }}
{...props}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,11 @@ export const DisplaySettingsButton = ({
return (
<Popover>
<PopoverTrigger asChild>
<ExplorerToolbarAction disabled={disabled} icon={<Settings2 />} tooltip="Result settings" />
<ExplorerToolbarAction
disabled={disabled}
icon={<Settings2 size={16} strokeWidth={2} />}
tooltip="Result settings"
/>
</PopoverTrigger>
<PopoverContent
side="bottom"
Expand Down
Loading
Loading