Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
61fe442
fix: increase grid preset label size from tiny to labels
kof Mar 12, 2026
7aeeda3
fix: use small variant for grid preset labels
kof Mar 12, 2026
8a5fded
feat: add workspaces feature flag (Stage 1)
kof Mar 12, 2026
57568c5
feat(workspaces): add database schema and migration (Stage 2)\n\n- Ad…
kof Mar 12, 2026
8d2f182
feat(workspaces): default workspace provisioning (Stage 3)
kof Mar 12, 2026
5528c8f
feat(workspaces): rename default workspace to "My workspace"
kof Mar 12, 2026
629f489
feat(workspaces): workspace CRUD backend (Stage 4)
kof Mar 12, 2026
95417c2
feat(workspaces): project-workspace binding (Stage 5)
kof Mar 12, 2026
95e40a4
fix(workspaces): use URL query param instead of cookie for workspace …
kof Mar 12, 2026
f3969d4
feat(workspaces): workspace member management backend (Stage 6)
kof Mar 12, 2026
6ce9fcb
feat: workspace members get access to workspace projects\n\n- Add Wor…
kof Mar 12, 2026
899166a
feat: stage 8 — dashboard workspace selector UI\n\n- Add WorkspaceSel…
kof Mar 12, 2026
85d56fd
feat: workspace selector with ghost Select, remove collapsible sectio…
kof Mar 12, 2026
5240422
feat: workspace management dialogs and UI improvements
kof Mar 13, 2026
f44c78a
feat: workspace ownership-based UI restrictions and avatar
kof Mar 13, 2026
064acbb
feat: delete workspace with projects and redirect on stale workspace URL
kof Mar 13, 2026
9614f08
fix: code review fixes — shared soft-delete, project ownership, cleanup
kof Mar 14, 2026
a778bbf
fix: workspace security hardening and cleanup\n\n- Add membership che…
kof Mar 14, 2026
5f22725
fix: add WorkspaceMember FK/index, clone workspace inheritance, addMe…
kof Mar 14, 2026
2453d4b
feat: add workspace selector and dialogs stories, fix missing feature…
kof Mar 14, 2026
b3ac0cc
feat: workspace member permissions and plan-based action gating
kof Mar 14, 2026
9e7139d
refactor: rename MemberRelation → WorkspaceRelation, remove permissio…
kof Mar 14, 2026
ee4913a
refactor: rename "Manage members" to "Members", remove menu icons
kof Mar 14, 2026
b891d33
::migrate::workspaces schema
kof Mar 16, 2026
6e12ef0
Update menu.stories.tsx
kof Mar 16, 2026
aae1ceb
feat: dashboard welcome view, profile name, and template approval bypass
kof Mar 16, 2026
f87454e
feat: gate workspace features behind maxWorkspaces plan limit
kof Mar 16, 2026
ffe3a8e
fix: filter null relations in workspace authorization query
kof Mar 16, 2026
42a2685
fix: move Free badge outside profile button to prevent layout overflow
kof Mar 16, 2026
5721faf
feat: refactor dashboard sidebar to Grid layout with workspace separa…
kof Mar 16, 2026
391984e
feat: always show workspace selector, minimum 1 workspace, gate membe…
kof Mar 16, 2026
6bffc61
feat: workspace downgrade — revoke member access when owner's plan drops
kof Mar 16, 2026
dd984ab
feat: make workspaces non-optional in getPermissions and DashboardData
kof Mar 16, 2026
600b4d7
feat: notification pipeline and workspace invite flow
kof Mar 17, 2026
5902b84
feat: add project transfer and move functionality
kof Mar 17, 2026
b240501
test: add canTransfer to permissions test expectations
kof Mar 17, 2026
3d3cf89
::migrate::
kof Mar 17, 2026
2cf0654
feat: add stories for transfer dialog and notification popover
kof Mar 17, 2026
c373334
::migrate:: apply workspace and notification migrations
kof Mar 17, 2026
8ca7e7d
fix: transfer dialog defaults to current workspace, adds separator be…
kof Mar 17, 2026
40bf534
fix: transfer dialog uses DropdownMenu for workspace selection, full-…
kof Mar 17, 2026
0aa949f
refactor: extract shared WorkspaceDropdown component\n\n- Create work…
kof Mar 17, 2026
36c1454
fix: notification popover story renders open with mock data, remove i…
kof Mar 17, 2026
2dbad88
feat: revoke invite wiring, store, maxProjectsAllowedPerUser, transf…
kof Mar 17, 2026
adfeeb3
feat: real-time notification polling with toast and browser alerts
kof Mar 18, 2026
9b76e38
feat: cross-tab polling deduplication for Polly
kof Mar 18, 2026
3eef2cf
refactor: decouple notification popover from notification types
kof Mar 18, 2026
9d97f64
fix: dashboard stories crashing on missing notifications
kof Mar 18, 2026
48357b7
fix: remove OwnedWorkspaces story, fix notification typecheck errors\…
kof Mar 18, 2026
4559dcf
::migrate:: fix: workspace review fixes — auth view, clone errors, re…
kof Mar 19, 2026
eb87893
::migrate::
kof Mar 19, 2026
11ab833
ci: allow feat/** branches and ::migrate:: anywhere in message ::migr…
kof Mar 19, 2026
fbb7e42
fix: allow migrate trigger anywhere and fix builder typecheck ::migra…
kof Mar 19, 2026
244252b
ci: gate migrate follow-up jobs to ::migrate:: commits ::migrate::
kof Mar 19, 2026
758c328
revert: restore original migrate.yaml workflow
kof Mar 19, 2026
f977219
::migrate:: run workspace migration on dev
kof Mar 19, 2026
1d67599
fix: allow workspace members to open projects in builder
kof Mar 19, 2026
7003770
fix: require notification for cross-owner project transfers
kof Mar 19, 2026
63fca43
fix: auto-refresh dashboard when project count changes
kof Mar 20, 2026
a0dc4d2
fix: allow viewers to open projects in builder
kof Mar 20, 2026
de12229
add swign animation for the bell
kof Mar 21, 2026
f21da6f
feat: animate notification bell with swing animation
kof Mar 21, 2026
9ab20b4
feat(workspace): add maxSeats, seat enforcement, usage UI, and access…
kof Apr 2, 2026
cdafcf2
refactor(workspace): consolidate dashboard workspace code into worksp…
kof Apr 2, 2026
735f897
refactor(workspace): split dialogs.tsx into one file per dialog
kof Apr 2, 2026
0f02142
refactor(workspace): inline WorkspaceDropdown into selector.tsx
kof Apr 2, 2026
4b6ea4c
refactor(workspace): delete dialogs.tsx barrel, import dialog modules…
kof Apr 2, 2026
43dd671
refactor(workspace): extract stores, rename modules, move transfer di…
kof Apr 2, 2026
c0da093
feat(notifications): unify notifications into shared feature, add bel…
kof Apr 2, 2026
e7802d2
refactor: rename WorkspaceRelation→Role, split schema.ts into focused…
kof Apr 2, 2026
a958149
feat(dashboard): restore last dashboard search on return from builder
kof Apr 2, 2026
15ed652
Update migration.sql
kof Apr 2, 2026
54178d6
feat(workspace): add seats management UI and POST /api/seats route
kof Apr 2, 2026
7a12802
Merge branch 'main' into feat/workspaces
kof Apr 3, 2026
5b24730
fix(trpc-interface): move Role type definition to trpc-interface to b…
kof Apr 3, 2026
7661354
refactor(project): delete role-schema.ts, move all role exports to tr…
kof Apr 3, 2026
98bbd26
feat(builder): notify users when a new builder version is deployed
kof Apr 3, 2026
88701cf
Update .env
kof Apr 8, 2026
3c39fa0
refactor: replace USER_PLAN with PLANS env, per-product plan lookup, …
kof Apr 8, 2026
7d64f1c
feat(workspaces): per-seat billing, suspension UX, dev plan via DB
kof Apr 8, 2026
35a62d4
fix(lint): add braces to if statement in parsePlansEnv
kof Apr 8, 2026
cede99a
Update secret-login.tsx
kof Apr 8, 2026
043bfc0
fix(typecheck): optional chaining for devPlanNames in secret-login
kof Apr 8, 2026
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
39 changes: 32 additions & 7 deletions apps/builder/.env
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@
DATABASE_URL=postgresql://postgres:pass@localhost/webstudio?pgbouncer=true
DIRECT_URL=postgresql://postgres:pass@localhost/webstudio

AUTH_SECRET="# Linux: $(openssl rand -hex 32) or go to https://generate-secret.now.sh/64"
AUTH_SECRET=0000

# GH_CLIENT_SECRET=
# GH_CLIENT_ID=
DEV_LOGIN=true

# Restrictions
MAX_ASSETS_PER_PROJECT=50

POSTGREST_URL=http://localhost:3000
# JWT token, decode using jwt.io. Encoded with PGRST_JWT_SECRET from docker-compose.yml
POSTGREST_API_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImttZHBpeHpvcWlpcmJtcGRpcHB5Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2NjYzMzY0MjgsImV4cCI6MTk4MTkxMjQyOH0.jjeYvTDrWP9pV7dfZr6fptualNQ3aR13kuPhvT25Sso"
Expand Down Expand Up @@ -49,9 +47,36 @@ POSTGREST_API_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIs
# BUILD_REQUIRE_SUBDOMAIN=

# Enable features behind a flag, * enables all.
FEATURES=*
# Current user plan features (default)
USER_PLAN=pro
FEATURE_FLAGS=*
# Plan definitions. JSON array of {name, extends?, features} objects.
# extends: inherit all features from a named parent plan, then override.
# Multiple active plans are merged (booleans: OR, numbers: max).
# Schema: packages/trpc-interface/src/shared/plan-features.ts
PLANS='[
{
"name": "Pro",
"features": {
"canDownloadAssets": true,
"canRestoreBackups": true,
"allowAdditionalPermissions": true,
"allowDynamicData": true,
"allowContentMode": true,
"allowStagingPublish": true,
"maxContactEmailsPerProject": 5,
"maxDomainsAllowedPerUser": 200,
"maxDailyPublishesPerUser": 100,
"maxProjectsAllowedPerUser": 300,
"maxAssetsPerProject": 350
}
},
{
"name": "Team",
"extends": "Pro",
"features": {
"maxWorkspaces": 20
}
}
]'


# TRCP server url and API token. If empty local TRPC server is used
Expand Down
2 changes: 0 additions & 2 deletions apps/builder/.env.production

This file was deleted.

6 changes: 5 additions & 1 deletion apps/builder/app/auth/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ export type LoginProps = {
isGithubEnabled?: boolean;
isGoogleEnabled?: boolean;
isSecretLoginEnabled?: boolean;
devPlanNames?: string[];
};

export const Login = ({
errorMessage,
isGithubEnabled,
isGoogleEnabled,
isSecretLoginEnabled,
devPlanNames,
}: LoginProps) => {
globalStyles();
return (
Expand Down Expand Up @@ -86,7 +88,9 @@ export const Login = ({
Sign in with GitHub
</Button>
</Form>
{isSecretLoginEnabled && <SecretLogin />}
{isSecretLoginEnabled && (
<SecretLogin devPlanNames={devPlanNames} />
)}
</Flex>
</TooltipProvider>
{errorMessage ? (
Expand Down
22 changes: 19 additions & 3 deletions apps/builder/app/auth/secret-login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { Button, Flex, InputField, theme } from "@webstudio-is/design-system";
import { useState } from "react";
import { authPath } from "~/shared/router-utils";

export const SecretLogin = () => {
type SecretLoginProps = {
devPlanNames?: string[];
};

export const SecretLogin = ({ devPlanNames }: SecretLoginProps) => {
const [show, setShow] = useState(false);
if (show) {
return (
Expand All @@ -11,16 +15,28 @@ export const SecretLogin = () => {
action={authPath({ provider: "dev" })}
style={{ display: "contents" }}
>
<Flex gap="2">
<Flex gap="2" direction="column">
<InputField
name="secret"
type="text"
minLength={2}
required
autoFocus
placeholder="Auth secret"
css={{ flexGrow: 1 }}
/>
<InputField
name="email"
type="email"
placeholder="Email (optional)"
/>
<select name="devPlan">
<option value="">Default plan</option>
{devPlanNames?.map((name) => (
<option key={name} value={name}>
{name}
</option>
))}
</select>
<Button type="submit">Login</Button>
</Flex>
</form>
Expand Down
41 changes: 28 additions & 13 deletions apps/builder/app/builder/builder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
rawTheme,
} from "@webstudio-is/design-system";
import type { AuthPermit } from "@webstudio-is/trpc-interface/index.server";
import type { Role } from "@webstudio-is/project";
import { initializeClientSync, getSyncClient } from "~/shared/sync/sync-client";
import { usePreventUnload } from "~/shared/sync/project-queue";
import { usePublish, $publisher } from "~/shared/pubsub";
Expand All @@ -32,7 +33,7 @@ import {
$authTokenPermissions,
$isDesignMode,
$isContentMode,
$userPlanFeatures,
setSharedStores,
subscribeModifierKeys,
$stagingUsername,
$stagingPassword,
Expand All @@ -44,7 +45,10 @@ import { useSyncPageUrl } from "~/shared/pages";
import { useMount, useUnmount } from "~/shared/hook-utils/use-mount";
import { subscribeCommands } from "~/builder/shared/commands";
import { ProjectSettings } from "~/shared/project-settings";
import type { UserPlanFeatures } from "~/shared/db/user-plan-features.server";
import type {
PlanFeatures,
Purchase,
} from "@webstudio-is/trpc-interface/plan-features";
import {
$activeSidebarPanel,
$dataLoadingState,
Expand Down Expand Up @@ -74,6 +78,10 @@ import {
import { useInertHandlers } from "./shared/inert-handlers";
import { TextToolbar } from "./features/workspace/canvas-tools/text-toolbar";
import { RemoteDialog } from "./features/help/remote-dialog";
import {
startSubscription,
stopSubscription,
} from "~/shared/notifications/subscription";
import type { SidebarPanelName } from "./sidebar-left/types";
import { SidebarLeft } from "./sidebar-left/sidebar-left";
import { useDisableContextMenu } from "./shared/use-disable-context-menu";
Expand Down Expand Up @@ -226,28 +234,31 @@ export type BuilderProps = {
projectId: string;
authToken?: string;
authPermit: AuthPermit;
role: Role | "own";
authTokenPermissions: TokenPermissions;
userPlanFeatures: UserPlanFeatures;
planFeatures: PlanFeatures;
purchases: Array<Purchase>;
stagingUsername: string;
stagingPassword: string;
};

export const Builder = ({
projectId,
authToken,
authPermit,
userPlanFeatures,
authTokenPermissions,
stagingUsername,
stagingPassword,
}: BuilderProps) => {
export const Builder = (props: BuilderProps) => {
const {
projectId,
authToken,
authPermit,
authTokenPermissions,
stagingUsername,
stagingPassword,
} = props;

useMount(initBuilderApi);

useMount(() => {
// additional data stores
$authPermit.set(authPermit);
$authToken.set(authToken);
$userPlanFeatures.set(userPlanFeatures);
setSharedStores(props);
$authTokenPermissions.set(authTokenPermissions);
$stagingUsername.set(stagingUsername);
$stagingPassword.set(stagingPassword);
Expand Down Expand Up @@ -283,6 +294,10 @@ export const Builder = ({
useToastErrors();
useEffect(subscribeCommands, []);
useEffect(subscribeResources, []);
useEffect(() => {
startSubscription();
return stopSubscription;
}, []);
useDisableContextMenu();

useUnmount(() => {
Expand Down
6 changes: 3 additions & 3 deletions apps/builder/app/builder/features/builder-mode.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
$builderMode,
$authPermit,
$authToken,
$userPlanFeatures,
$planFeatures,
} from "~/shared/nano-states";

export default {
Expand All @@ -15,8 +15,8 @@ export const BuilderMode = () => {
$builderMode.set("design");
$authPermit.set("own");
$authToken.set(undefined);
$userPlanFeatures.set({
...$userPlanFeatures.get(),
$planFeatures.set({
...$planFeatures.get(),
allowContentMode: true,
});

Expand Down
15 changes: 8 additions & 7 deletions apps/builder/app/builder/features/menu/menu.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
$authToken,
$authTokenPermissions,
$builderMode,
$userPlanFeatures,
$purchases,
$planFeatures,
} from "~/shared/nano-states";

export default {
Expand All @@ -21,11 +22,11 @@ const OwnerDesignModeVariant = () => {
canCopy: true,
canPublish: true,
});
$userPlanFeatures.set({
...$userPlanFeatures.get(),
$planFeatures.set({
...$planFeatures.get(),
allowContentMode: true,
purchases: [{ planName: "Pro" }],
});
$purchases.set([{ planName: "Pro" }]);
return (
<Toolbar>
<Menu defaultOpen />
Expand Down Expand Up @@ -58,11 +59,11 @@ const AdminContentModeVariant = () => {
canCopy: true,
canPublish: true,
});
$userPlanFeatures.set({
...$userPlanFeatures.get(),
$planFeatures.set({
...$planFeatures.get(),
allowContentMode: true,
purchases: [{ planName: "Pro" }],
});
$purchases.set([{ planName: "Pro" }]);
return (
<Toolbar>
<Menu defaultOpen />
Expand Down
7 changes: 3 additions & 4 deletions apps/builder/app/builder/features/menu/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
$authToken,
$authTokenPermissions,
$isDesignMode,
$userPlanFeatures,
$purchases,
} from "~/shared/nano-states";
import { emitCommand } from "~/builder/shared/commands";
import { MenuButton } from "./menu-button";
Expand Down Expand Up @@ -58,8 +58,7 @@ const ViewMenuItem = () => {
};

export const Menu = ({ defaultOpen }: { defaultOpen?: boolean } = {}) => {
const userPlanFeatures = useStore($userPlanFeatures);
const hasPaidPlan = userPlanFeatures.purchases.length > 0;
const purchases = useStore($purchases);
const authPermit = useStore($authPermit);
const authTokenPermission = useStore($authTokenPermissions);
const authToken = useStore($authToken);
Expand Down Expand Up @@ -285,7 +284,7 @@ export const Menu = ({ defaultOpen }: { defaultOpen?: boolean } = {}) => {
</DropdownMenuSubContent>
</DropdownMenuSub>

{hasPaidPlan === false && (
{purchases.length === 0 && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ $project.set({
tags: [],

marketplaceApprovalStatus: "UNLISTED",
workspaceId: null,

latestStaticBuild: null,
previewImageAssetId: null,
Expand Down
8 changes: 4 additions & 4 deletions apps/builder/app/builder/features/pages/page-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ import {
$pages,
$publishedOrigin,
$project,
$userPlanFeatures,
$permissions,
$isDesignMode,
} from "~/shared/nano-states";
import { $openProjectSettings } from "~/shared/nano-states/project-settings";
Expand Down Expand Up @@ -320,7 +320,7 @@ const PathField = ({
value: string;
onChange: (value: string) => void;
}) => {
const { allowDynamicData } = useStore($userPlanFeatures);
const { allowDynamicData } = useStore($permissions);
const id = useId();
return (
<Grid gap={1}>
Expand Down Expand Up @@ -464,7 +464,7 @@ const RedirectField = ({
onChange: (value: string) => void;
}) => {
const id = useId();
const { allowDynamicData } = useStore($userPlanFeatures);
const { allowDynamicData } = useStore($permissions);
const { variableValues, scope, aliases } = useStore($pageRootScope);
return (
<Grid gap={1}>
Expand Down Expand Up @@ -692,7 +692,7 @@ const FormFields = ({
const fieldIds = useIds(fieldNames);
const assets = useStore($assets);
const pages = useStore($pages);
const { allowDynamicData } = useStore($userPlanFeatures);
const { allowDynamicData } = useStore($permissions);
const { variableValues, scope, aliases } = useStore($pageRootScope);

const pageUrl = usePageUrl(values);
Expand Down
Loading
Loading