Implementation Plan: Onboarding Wizard v2
Translates the approved
spec.mdinto an architecture and tech-choice plan. The plan owns implementation details; the spec owns behaviour.
Feature ID: onboarding-wizard-v2
Spec: ./spec.md
Tasks: ./tasks.md
Status: Draft
Last updated: 2026-05-11
1. Architecture summary
Reuses without change:
OnboardingPluginStepandPluginOnboardingWizardcomponents — wrapped, not replaced.- All AI provider plugins (
openrouter,claude-codewith its dualoauthToken+apiKey,codex,geminiafter FR-27 extension). githubplugin OAuth + GitHub App flows.vercelplugin.k8splugin's settings schema and validation.@ever-works/monitoringAnalyticsService(server-side PostHog).GitFacade.createRepositoryinterface — we add a new branch, not a new façade.
Net-new:
apps/api/src/onboarding/controller, service, DTOs for state + catalog endpoints.users.onboarding_completed_at/onboarding_dismissed_at/onboarding_statecolumns.works.storage_provider/works.deploy_providercolumns.packages/agent/src/git/ever-works-git.provider.ts+EverWorksGitProviderservice.packages/agent/src/deployment/ever-works-k8s.provider.ts+EverWorksK8sDeployProviderservice.packages/plugins/grok/package.apiKeysetting on the existinggeminiplugin.- Web: rewritten
EverWorksOnboardingWizard.tsxdriven byuseOnboardingFlow, plus choice-card + plugins-catalog primitives. - Server action
trackOnboardingEventinapps/web/src/app/actions/onboarding/track.ts.
2. Tech choices
| Concern | Choice | Rationale |
|---|---|---|
| State persistence | TypeORM columns on users; jsonb for onboarding_state | Matches existing user-scoped flag pattern; jsonb avoids a side table |
| State API | New apps/api/src/onboarding/ Nest module with OnboardingStateController | Follows existing per-feature module structure |
| Catalog API | Same controller, GET /api/onboarding/catalog reads env-flag status server-side | Server is authoritative on which Ever Works defaults are enabled |
| Wizard state hook | Custom useOnboardingFlow using useReducer + SWR for state sync | Avoids adding a new dep; SWR already in apps/web for similar patterns |
| Telemetry | Server action calling @ever-works/monitoring AnalyticsService.track | Reuses server-side PostHog client; no posthog-js bundle growth |
| Ever Works Git auth | Octokit with PAT from EVER_WORKS_CUSTOMERS_GITHUB_PAT env var | Same library the github plugin already uses |
| Ever Works Deploy auth | Build a k8s plugin config object at call time from env vars; pass to existing k8s deploy primitives | Reuses validated k8s plugin code; no kubeconfig persisted per user |
| Quota check | Indexed COUNT(*) on works where user_id = ? AND deploy_provider = 'ever-works' AND status NOT IN ('deleted','archived') | One round-trip; explicit non-negotiable index on (user_id, deploy_provider) |
| Grok integration | LangChain @langchain/openai with baseURL: https://api.x.ai/v1/ | xAI is OpenAI-API-compatible; matches how groq plugin works |
| Gemini API key surfacing | Add optional apiKey field to existing plugin schema; CLI path unchanged when empty | Minimum-diff; matches FR-27 expectation |
| Logos | Vendor official SVG marks under apps/web/public/logos/ (Apache/CC0 sources) | Static, predictable, no third-party fetch |
| Tests (agent) | Jest, mocking Octokit + k8s plugin entrypoints | Matches existing pattern in packages/agent/src/__tests__/ |
| Tests (web) | Vitest for hook unit tests; Playwright for the end-to-end happy path | Matches existing split between web unit and e2e tests |
3. Data model
3.1 users columns (one migration)
ALTER TABLE users
ADD COLUMN onboarding_completed_at timestamptz NULL,
ADD COLUMN onboarding_dismissed_at timestamptz NULL,
ADD COLUMN onboarding_state jsonb NULL;
onboarding_state payload shape:
{
version: 2,
lastStep: number,
ai: { choice: 'ever-works' | 'openrouter' | 'claude-code' | 'codex' | 'gemini' | 'grok' },
storage: { choice: 'ever-works-git' | 'user-github' | 'user-gitlab' | 'user-git' },
deploy: { choice: 'ever-works' | 'vercel' | 'k8s' },
skippedSteps: string[],
pluginsReviewed: boolean
}
3.2 works columns (same migration or paired)
ALTER TABLE works
ADD COLUMN storage_provider varchar(32) NOT NULL DEFAULT 'user-github',
ADD COLUMN deploy_provider varchar(32) NOT NULL DEFAULT 'vercel';
CREATE INDEX idx_works_user_deploy_active
ON works (user_id, deploy_provider)
WHERE status NOT IN ('deleted','archived');
Defaults are conservative ("Your GitHub" / "Vercel") so any pre-existing rows
behave exactly like today's hard-coded path. New rows pick up the user's
onboarding choice via WorksService.create.
4. New env vars
All registered via @nestjs/config with class-validator schema in
apps/api/src/config/. Documented in .env.example.
# Storage — Ever Works Git
STORAGE_EVER_WORKS_GIT_ENABLED=false
EVER_WORKS_CUSTOMERS_GITHUB_ORG=ever-works-cloud
EVER_WORKS_CUSTOMERS_GITHUB_PAT=
EVER_WORKS_CUSTOMERS_GITHUB_VISIBILITY=private # private | public
# Deploy — Ever Works (k8s tenant cluster)
DEPLOY_EVER_WORKS_ENABLED=false
EVER_WORKS_DEPLOY_KUBECONFIG= # full contents or use _PATH
EVER_WORKS_DEPLOY_KUBECONFIG_PATH=
EVER_WORKS_DEPLOY_NAMESPACE=ever-works-tenants
EVER_WORKS_DEPLOY_INGRESS_HOST_TEMPLATE={slug}.ever.works
EVER_WORKS_DEPLOY_INGRESS_CLASS=nginx
EVER_WORKS_DEPLOY_TLS_ISSUER=letsencrypt-prod
EVER_WORKS_DEPLOY_REGISTRY=
EVER_WORKS_DEPLOY_MAX_WORKS_PER_USER=3
The catalog endpoint (GET /api/onboarding/catalog) surfaces the boolean
flags to the web app so the wizard renders the matching cards as Planned
when the underlying provider is disabled.
5. Server-side modules
5.1 apps/api/src/onboarding/
onboarding.module.ts— registers controllers + service + telemetry hook.onboarding-state.controller.ts—GET/PATCH /api/onboarding/state,POST /api/onboarding/complete,POST /api/onboarding/dismiss.onboarding-catalog.controller.ts—GET /api/onboarding/catalog.onboarding-state.service.ts— reads/writesusers.onboarding_*via repository.onboarding-catalog.service.ts— composes catalog from plugin manifests + env flags.dto/{state,patch-state,catalog}.dto.ts— class-validator + Swagger annotations.
5.2 packages/agent/src/git/ever-works-git.provider.ts
Implements the existing GitProvider shape used by GitFacade:
createRepository(work)— OctokitPOST /orgs/{org}/repos, private by default.push(work, content)— same Octokit, using the platform PAT.delete(work)— soft-only on the Ever Works side (we don't yank user data).- Every call wraps in
ActivityLogService.record({ actorKind: 'platform', userId, action, resource }).
5.3 packages/agent/src/deployment/ever-works-k8s.provider.ts
getDeployConfig(user)— builds ak8splugin config object from env vars, substituting{slug}in the ingress host template.ensureNamespace(user)— idempotent create of{base-namespace}-{userId}.deploy(work)— calls the existingk8splugin's deploy primitive with the env-derived config.checkQuota(userId)— runs the indexed COUNT and throws a typedQuotaExceededErrorwhen ≥EVER_WORKS_DEPLOY_MAX_WORKS_PER_USER.
5.4 packages/agent/src/facades/git.facade.ts (existing file)
Add a storageProvider branch in createRepository:
switch (work.storageProvider) {
case 'ever-works-git':
return this.everWorksGit.createRepository(work);
case 'user-github':
default:
return this.userGithub.createRepository(work /* organization */);
}
5.5 WorksService.create (existing)
- Read
users.onboarding_statefor the current user. - Set
work.storageProvider = onboardingState.storage.choice(defaultuser-githubif no state). - Set
work.deployProvider = onboardingState.deploy.choice(defaultvercel). - If
deployProvider === 'ever-works'ANDDEPLOY_EVER_WORKS_ENABLED, callEverWorksK8sDeployProvider.checkQuota(userId)before persisting.
6. Plugin work
6.1 New: packages/plugins/grok/
Scaffolded by copying packages/plugins/groq/ (closest existing analog) and adjusting:
package.json—everworks.plugin.id = 'grok', name "Grok (xAI)", categoryai-provider, capabilities["ai-provider"].uiHints.includeInOnboarding: true,onboardingPriority: 3.src/grok.plugin.ts— extendsBaseAiProvider,providerType = 'openai',baseURL = https://api.x.ai/v1/. Settings:apiKey(x-secret,x-envVar: XAI_API_KEY),defaultModel(defaultgrok-2-latest), tieredsimple/medium/complexmodels,temperature,maxTokens.- Vitest spec covering manifest shape,
connectionValidationhappy + sad path with mockedfetch.
6.2 Modify: packages/plugins/gemini/src/gemini.plugin.ts
Add to settingsSchema.properties:
apiKey: {
type: 'string',
title: 'Gemini API Key',
description: 'Use a direct Gemini API key. Leave empty to fall back to the local Gemini CLI.',
'x-secret': true,
'x-envVar': 'GEMINI_API_KEY',
'x-scope': 'user'
}
CLI-mode path must still work when apiKey is empty (existing behaviour).
Update the plugin spec to assert both states.
6.3 Modify: packages/plugins/k8s/package.json
Flip everworks.plugin.uiHints.includeInOnboarding from false to true,
add onboardingPriority: 4 and onboardingDescription. No code changes.
7. Web changes
7.1 New components (apps/web/src/components/onboarding/)
EverWorksOnboardingWizard.tsx— rewritten driver; no longer iterates plugins.useOnboardingFlow.ts—useReducerover a step machine; SWR-syncs to/api/onboarding/stateon every transition.steps/WelcomeStep.tsx,AIChoiceStep.tsx,AIConfigStep.tsx,StorageChoiceStep.tsx,StorageConfigStep.tsx,DeployChoiceStep.tsx,DeployConfigStep.tsx,PluginsCatalogStep.tsx,CreateWorkStep.tsx.ChoiceCardGrid.tsx,ChoiceCard.tsx,PluginsCatalogGrid.tsx.WizardFooter.tsx— Back + Skip + Refresh + Next.
7.2 Updated
apps/web/src/app/[locale]/(dashboard)/layout.tsx— server-side fetch of state and catalog; pass into the client wizard.apps/web/src/app/[locale]/(dashboard)/layout-client.tsx— dropuseOnboardingStatelocalStorage path; render the new wizard.apps/web/src/components/onboarding/use-onboarding-state.ts— shrink to a thin SWR-style hook over the API; localStorage becomes offline-cache only.
7.3 New server action
apps/web/src/app/actions/onboarding/track.ts—'use server', accepts(event, props), server-side validates, callsAnalyticsService.track.
7.4 Static assets
apps/web/public/logos/{openrouter,anthropic,xai,google,vercel,github,gitlab,kubernetes,everworks}.svg.
8. Telemetry events
| Event | Required props | When |
| ----------------------------------------------- | ------------------------------------ | ------------------------------- | -------------- | ----------------------------- |
| onboarding_opened | trigger: 'auto' | 'badge' | 'help' | First render with wizard open |
| onboarding_closed | completed: bool, lastStepIndex | Modal closes |
| onboarding_completed | none beyond defaults | Server flag flipped |
| onboarding_step_viewed | stepKind, stepIndex, pluginId? | Step paints |
| onboarding_step_next / _back / _skipped | stepKind | Footer action |
| onboarding_ai_choice_selected | choice | AI step Next/Skip |
| onboarding_storage_choice_selected | choice | Storage step Next/Skip |
| onboarding_deploy_choice_selected | choice | Deploy step Next/Skip |
| onboarding_plugin_connected | pluginId, via: 'oauth' | 'fields' | 'device-auth' | A config step reports success |
| onboarding_plugin_refresh_clicked | pluginId | Refresh button on a config step |
| onboarding_planned_card_clicked | card | Click on a greyed Planned card |
| onboarding_byok_skipped | choice | Skip on a BYOK config step |
| onboarding_plugins_step_expanded | pluginId | A card opens its inline form |
| onboarding_plugins_step_skipped / _advanced | none | Step 8 exit path |
| onboarding_ever_works_quota_blocked | limit | API returns quota_exceeded |
All events carry userId and wizardVersion: 'v2'.
9. Failure modes
| Scenario | Behaviour |
|---|---|
/api/onboarding/state GET fails | Wizard renders with defaults; sets a banner; localStorage cache used if present |
PATCH /api/onboarding/state fails | UI keeps progress locally; retries on next step transition; toast on permanent failure |
| Telemetry call fails | Logged server-side; never blocks UI |
EVER_WORKS_CUSTOMERS_GITHUB_PAT invalid at runtime | API returns storage_provider_misconfigured; UI surfaces "We're working on it" toast; user can pick Your GitHub |
| Quota check race (two concurrent Work creates) | DB-level guard via INSERT … WHERE COUNT(active) < cap (or row-level lock on users) ensures one wins |
k8s env config invalid | Boot-time validation rejects; deploy never starts |
10. Rollout
Per the saved pre-launch instruction, no v2 feature flag or ramp ceremony:
- Migrations run on
develop. STORAGE_EVER_WORKS_GIT_ENABLEDandDEPLOY_EVER_WORKS_ENABLEDstay false in.env.example; flipped per environment once the org PAT and cluster are provisioned.- When either flag is false, the corresponding card renders as Planned in the wizard.
- Standard PR flow:
develop → stage → main.
11. PR breakdown (four commits on one branch)
- chore(plugins): scaffold
grok, addapiKeytogemini, flipk8s.includeInOnboarding, copy tweak inclaude-codeconfig wizard. - feat(agent):
EverWorksGitProvider,EverWorksK8sDeployProvider,git.facadestorage-branching,workscolumns + migration, quota service, activity-log integration. - feat(api):
/api/onboarding/*controllers,userscolumns + migration, catalog endpoint, server-side telemetry wiring. - feat(web): wizard rewrite, choice components, plugins catalog, logos, server action, layout integration.