Skip to content

Commit 3756622

Browse files
rafavallsclaude
andcommitted
feat(onboarding): add brand context storage, settings UI, and onboarding route
- Add brand_context column to organization_settings (migration 063) - Implement OrganizationSettingsStorage get/upsert with brand context - Add ORGANIZATION_SETTINGS_GET/UPDATE tools with brand context support - Add Brand Context settings page under Settings > Context - Add dynamic buildBrandContextPrompt() for decopilot prompt injection - Add onboarding route with email detection and org creation flow Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d84035a commit 3756622

14 files changed

Lines changed: 669 additions & 4 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Kysely } from "kysely";
2+
3+
export async function up(db: Kysely<unknown>): Promise<void> {
4+
await db.schema
5+
.alterTable("organization_settings")
6+
.addColumn("brand_context", "text")
7+
.execute();
8+
}
9+
10+
export async function down(db: Kysely<unknown>): Promise<void> {
11+
await db.schema
12+
.alterTable("organization_settings")
13+
.dropColumn("brand_context")
14+
.execute();
15+
}

apps/mesh/migrations/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import * as migration059kv from "./059-kv.ts";
6161
import * as migration060memberindex from "./060-member-index.ts";
6262
import * as migration061downstreamtokenconnectionindex from "./061-downstream-token-connection-index.ts";
6363
import * as migration062privateregistry from "./062-private-registry.ts";
64+
import * as migration063brandcontext from "./063-brand-context.ts";
6465

6566
/**
6667
* Core migrations for the Mesh application.
@@ -135,6 +136,7 @@ const migrations: Record<string, Migration> = {
135136
"061-downstream-token-connection-index":
136137
migration061downstreamtokenconnectionindex,
137138
"062-private-registry": migration062privateregistry,
139+
"063-brand-context": migration063brandcontext,
138140
};
139141

140142
export default migrations;

apps/mesh/src/api/routes/decopilot/constants.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,40 @@ Focus exclusively on:
143143
</scope>`;
144144
}
145145

146+
/**
147+
* Brand context prompt — org identity injected into every prompt.
148+
* The model must never use tools to look this up; it is always present.
149+
*/
150+
export function buildBrandContextPrompt(
151+
brand: {
152+
name: string;
153+
domain: string;
154+
overview: string;
155+
colors: { label: string; value: string }[];
156+
fonts: { name: string; role: string }[];
157+
} | null,
158+
): string {
159+
if (!brand) return "";
160+
161+
const primaryColors = brand.colors
162+
.map((c) => `${c.label}: ${c.value}`)
163+
.join(", ");
164+
const fonts = brand.fonts.map((f) => `${f.name} (${f.role})`).join(", ");
165+
166+
return `<brand-context>
167+
You are assisting the team at ${brand.name}. Always use this context to inform your responses — never call tools to look up information that is already provided here.
168+
169+
Company: ${brand.name}
170+
Website: https://${brand.domain}
171+
Domain: ${brand.domain}
172+
Overview: ${brand.overview}
173+
Primary colors: ${primaryColors}
174+
Fonts: ${fonts}
175+
176+
When the user mentions "my site", "our store", "the website", or "my URL", they are referring to https://${brand.domain}.
177+
</brand-context>`;
178+
}
179+
146180
export const TITLE_GENERATOR_PROMPT = `Generate a concise, sentence-case title (3-7 words) that captures the main topic or goal of this session. Use sentence case: capitalize only the first word and proper nouns.
147181
148182
Return JSON with a single "title" field.

apps/mesh/src/storage/organization-settings.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ export class OrganizationSettingsStorage
3535
? JSON.parse(record.registry_config)
3636
: record.registry_config
3737
: null,
38+
brand_context: record.brand_context
39+
? typeof record.brand_context === "string"
40+
? JSON.parse(record.brand_context)
41+
: record.brand_context
42+
: null,
3843
createdAt: record.createdAt,
3944
updatedAt: record.updatedAt,
4045
};
@@ -45,7 +50,10 @@ export class OrganizationSettingsStorage
4550
data?: Partial<
4651
Pick<
4752
OrganizationSettings,
48-
"sidebar_items" | "enabled_plugins" | "registry_config"
53+
| "sidebar_items"
54+
| "enabled_plugins"
55+
| "registry_config"
56+
| "brand_context"
4957
>
5058
>,
5159
): Promise<OrganizationSettings> {
@@ -59,6 +67,9 @@ export class OrganizationSettingsStorage
5967
const registryConfigJson = data?.registry_config
6068
? JSON.stringify(data.registry_config)
6169
: null;
70+
const brandContextJson = data?.brand_context
71+
? JSON.stringify(data.brand_context)
72+
: null;
6273

6374
await this.db
6475
.insertInto("organization_settings")
@@ -67,6 +78,7 @@ export class OrganizationSettingsStorage
6778
sidebar_items: sidebarItemsJson,
6879
enabled_plugins: enabledPluginsJson,
6980
registry_config: registryConfigJson,
81+
brand_context: brandContextJson,
7082
createdAt: now,
7183
updatedAt: now,
7284
})
@@ -75,6 +87,7 @@ export class OrganizationSettingsStorage
7587
sidebar_items: sidebarItemsJson ? sidebarItemsJson : undefined,
7688
enabled_plugins: enabledPluginsJson ? enabledPluginsJson : undefined,
7789
registry_config: registryConfigJson ? registryConfigJson : undefined,
90+
brand_context: brandContextJson ? brandContextJson : undefined,
7891
updatedAt: now,
7992
}),
8093
)
@@ -88,6 +101,7 @@ export class OrganizationSettingsStorage
88101
sidebar_items: data?.sidebar_items ?? null,
89102
enabled_plugins: data?.enabled_plugins ?? null,
90103
registry_config: data?.registry_config ?? null,
104+
brand_context: data?.brand_context ?? null,
91105
createdAt: now,
92106
updatedAt: now,
93107
};

apps/mesh/src/storage/ports.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,10 @@ export interface OrganizationSettingsStoragePort {
131131
data?: Partial<
132132
Pick<
133133
OrganizationSettings,
134-
"sidebar_items" | "enabled_plugins" | "registry_config"
134+
| "sidebar_items"
135+
| "enabled_plugins"
136+
| "registry_config"
137+
| "brand_context"
135138
>
136139
>,
137140
): Promise<OrganizationSettings>;

apps/mesh/src/storage/types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,24 @@ export interface RegistryConfig {
131131
blockedMcps: string[];
132132
}
133133

134+
export interface BrandContext {
135+
name: string;
136+
domain: string;
137+
overview: string;
138+
logo?: string;
139+
favicon?: string;
140+
ogImage?: string;
141+
fonts?: Array<{ name: string; role: string }>;
142+
colors?: Array<{ label: string; value: string }>;
143+
images?: string[];
144+
}
145+
134146
export interface OrganizationSettingsTable {
135147
organizationId: string;
136148
sidebar_items: JsonArray<SidebarItem[]> | null;
137149
enabled_plugins: JsonArray<string[]> | null;
138150
registry_config: JsonObject<RegistryConfig> | null;
151+
brand_context: JsonObject<BrandContext> | null;
139152
createdAt: ColumnType<Date, Date | string, never>;
140153
updatedAt: ColumnType<Date, Date | string, Date | string>;
141154
}
@@ -145,6 +158,7 @@ export interface OrganizationSettings {
145158
sidebar_items: SidebarItem[] | null;
146159
enabled_plugins: string[] | null;
147160
registry_config: RegistryConfig | null;
161+
brand_context: BrandContext | null;
148162
createdAt: Date | string;
149163
updatedAt: Date | string;
150164
}

apps/mesh/src/tools/organization/schema.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,22 @@ export type SidebarItem = z.infer<typeof SidebarItemSchema>;
2424
* Controls which registries are visible in the store and which individual MCPs are blocked.
2525
* When null/absent, defaults to Deco Store enabled with nothing blocked.
2626
*/
27+
export const BrandContextSchema = z.object({
28+
name: z.string(),
29+
domain: z.string(),
30+
overview: z.string(),
31+
logo: z.string().optional(),
32+
favicon: z.string().optional(),
33+
ogImage: z.string().optional(),
34+
fonts: z.array(z.object({ name: z.string(), role: z.string() })).optional(),
35+
colors: z
36+
.array(z.object({ label: z.string(), value: z.string() }))
37+
.optional(),
38+
images: z.array(z.string()).optional(),
39+
});
40+
41+
export type BrandContext = z.infer<typeof BrandContextSchema>;
42+
2743
export const RegistryConfigSchema = z.object({
2844
registries: z
2945
.record(z.string(), z.object({ enabled: z.boolean() }))

apps/mesh/src/tools/organization/settings-get.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { z } from "zod";
22
import { defineTool } from "../../core/define-tool";
33
import { requireAuth } from "../../core/mesh-context";
4-
import { SidebarItemSchema, RegistryConfigSchema } from "./schema.ts";
4+
import {
5+
BrandContextSchema,
6+
SidebarItemSchema,
7+
RegistryConfigSchema,
8+
} from "./schema.ts";
59

610
export const ORGANIZATION_SETTINGS_GET = defineTool({
711
name: "ORGANIZATION_SETTINGS_GET",
@@ -21,6 +25,7 @@ export const ORGANIZATION_SETTINGS_GET = defineTool({
2125
sidebar_items: z.array(SidebarItemSchema).nullable().optional(),
2226
enabled_plugins: z.array(z.string()).nullable().optional(),
2327
registry_config: RegistryConfigSchema.nullable().optional(),
28+
brand_context: BrandContextSchema.nullable().optional(),
2429
createdAt: z.string().datetime().optional().describe("ISO 8601 timestamp"),
2530
updatedAt: z.string().datetime().optional().describe("ISO 8601 timestamp"),
2631
}),

apps/mesh/src/tools/organization/settings-update.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { z } from "zod";
22
import { defineTool } from "../../core/define-tool";
33
import { requireAuth } from "../../core/mesh-context";
4-
import { SidebarItemSchema, RegistryConfigSchema } from "./schema.ts";
4+
import {
5+
BrandContextSchema,
6+
SidebarItemSchema,
7+
RegistryConfigSchema,
8+
} from "./schema.ts";
59

610
export const ORGANIZATION_SETTINGS_UPDATE = defineTool({
711
name: "ORGANIZATION_SETTINGS_UPDATE",
@@ -19,13 +23,15 @@ export const ORGANIZATION_SETTINGS_UPDATE = defineTool({
1923
sidebar_items: z.array(SidebarItemSchema).optional(),
2024
enabled_plugins: z.array(z.string()).optional(),
2125
registry_config: RegistryConfigSchema.optional(),
26+
brand_context: BrandContextSchema.optional(),
2227
}),
2328

2429
outputSchema: z.object({
2530
organizationId: z.string(),
2631
sidebar_items: z.array(SidebarItemSchema).nullable().optional(),
2732
enabled_plugins: z.array(z.string()).nullable().optional(),
2833
registry_config: RegistryConfigSchema.nullable().optional(),
34+
brand_context: BrandContextSchema.nullable().optional(),
2935
createdAt: z.string().datetime().describe("ISO 8601 timestamp"),
3036
updatedAt: z.string().datetime().describe("ISO 8601 timestamp"),
3137
}),
@@ -44,6 +50,7 @@ export const ORGANIZATION_SETTINGS_UPDATE = defineTool({
4450
sidebar_items: input.sidebar_items,
4551
enabled_plugins: input.enabled_plugins,
4652
registry_config: input.registry_config,
53+
brand_context: input.brand_context,
4754
},
4855
);
4956

apps/mesh/src/web/index.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ const storeInviteRoute = createRoute({
9898
),
9999
});
100100

101+
const onboardingRoute = createRoute({
102+
getParentRoute: () => rootRoute,
103+
path: "/onboarding",
104+
component: lazyRouteComponent(() => import("./routes/onboarding.tsx")),
105+
});
106+
101107
const oauthCallbackRoute = createRoute({
102108
getParentRoute: () => rootRoute,
103109
path: "/oauth/callback",
@@ -291,6 +297,14 @@ const settingsGeneralRoute = createRoute({
291297
),
292298
});
293299

300+
const settingsBrandContextRoute = createRoute({
301+
getParentRoute: () => settingsLayout,
302+
path: "/brand-context",
303+
component: lazyRouteComponent(
304+
() => import("./routes/orgs/settings/brand-context.tsx"),
305+
),
306+
});
307+
294308
const settingsFeaturesRoute = createRoute({
295309
getParentRoute: () => settingsLayout,
296310
path: "/features",
@@ -527,6 +541,7 @@ const settingsWithChildren = settingsLayout.addChildren([
527541
collectionDetailRoute,
528542
monitoringRoute,
529543
settingsGeneralRoute,
544+
settingsBrandContextRoute,
530545
settingsFeaturesRoute,
531546
settingsAiProvidersRoute,
532547
settingsMembersRoute,
@@ -571,6 +586,7 @@ const routeTree = rootRoute.addChildren([
571586
oauthCallbackRoute,
572587
oauthCallbackAiProviderRoute,
573588
storeInviteRoute,
589+
onboardingRoute,
574590
]);
575591

576592
const router = createRouter({

0 commit comments

Comments
 (0)