UNPKG

oneie

Version:

Build apps, websites, and AI agents in English. Zero-interaction setup for AI agents (Claude Code, Cursor, Windsurf). Download to your computer, run in the cloud, deploy to the edge. Open source and free forever.

408 lines (322 loc) 10.5 kB
--- title: Convex On Off dimension: things category: plans tags: architecture related_dimensions: events scope: global created: 2025-11-03 updated: 2025-11-03 version: 1.0.0 ai_context: | This document is part of the things dimension in the plans category. Location: one/things/plans/convex-on-off.md Purpose: Documents convex integration toggle plan Related dimensions: events For AI agents: Read this to understand convex on off. --- # Convex Integration Toggle Plan ## Overview Create a flexible architecture that allows the site to operate in three modes: 1. **Static Mode**: Pure static site with content collections only 2. **Database Mode**: Fully database-driven with Convex 3. **Hybrid Mode**: Content collections + Convex queries ## Architecture Strategy ### 1. Environment Configuration **Create feature flags in `src/config/features.ts`:** ```typescript export const FEATURES = { CONVEX_ENABLED: import.meta.env.CONVEX_ENABLED === "true", AUTH_ENABLED: import.meta.env.AUTH_ENABLED === "true", STATIC_MODE: import.meta.env.STATIC_MODE === "true", } as const; export type SiteMode = "static" | "database" | "hybrid"; export function getSiteMode(): SiteMode { if (FEATURES.STATIC_MODE) return "static"; if (FEATURES.CONVEX_ENABLED) return "hybrid"; return "static"; } ``` **Environment variables:** ```bash # .env.example CONVEX_ENABLED=false # Toggle Convex features AUTH_ENABLED=false # Toggle authentication STATIC_MODE=true # Force static site generation CONVEX_URL= # Only needed if CONVEX_ENABLED=true ``` ### 2. Adapter Configuration **Dynamic adapter selection in `astro.config.mjs`:** ```javascript import { defineConfig } from "astro/config"; import cloudflare from "@astrojs/cloudflare"; const isStatic = process.env.STATIC_MODE === "true"; const convexEnabled = process.env.CONVEX_ENABLED === "true"; export default defineConfig({ output: isStatic ? "static" : "server", adapter: isStatic ? undefined : cloudflare({ platformProxy: { enabled: convexEnabled }, }), // ... rest of config }); ``` ### 3. Data Layer Abstraction **Create unified data interface in `src/lib/data/index.ts`:** ```typescript import type { CollectionEntry } from "astro:content"; import { FEATURES } from "@/config/features"; export interface BlogPost { id: string; title: string; description: string; date: Date; author: string; tags: string[]; category: string; content?: string; image?: string; } export interface DataProvider { getBlogPosts(): Promise<BlogPost[]>; getBlogPost(slug: string): Promise<BlogPost | null>; // Add more methods as needed } class StaticDataProvider implements DataProvider { async getBlogPosts(): Promise<BlogPost[]> { const { getCollection } = await import("astro:content"); const posts = await getCollection("blog"); return posts.map(transformContentEntry); } async getBlogPost(slug: string): Promise<BlogPost | null> { const { getEntry } = await import("astro:content"); const post = await getEntry("blog", slug); return post ? transformContentEntry(post) : null; } } class ConvexDataProvider implements DataProvider { async getBlogPosts(): Promise<BlogPost[]> { const { api } = await import("convex/_generated/api"); const client = getConvexClient(); const posts = await client.query(api.blog.list); return posts.map(transformConvexPost); } async getBlogPost(slug: string): Promise<BlogPost | null> { const { api } = await import("convex/_generated/api"); const client = getConvexClient(); return await client.query(api.blog.getBySlug, { slug }); } } class HybridDataProvider implements DataProvider { private static: StaticDataProvider; private convex: ConvexDataProvider; async getBlogPosts(): Promise<BlogPost[]> { const [staticPosts, dbPosts] = await Promise.all([ this.static.getBlogPosts(), this.convex.getBlogPosts(), ]); return [...staticPosts, ...dbPosts].sort( (a, b) => b.date.getTime() - a.date.getTime(), ); } } export function getDataProvider(): DataProvider { const mode = getSiteMode(); switch (mode) { case "static": return new StaticDataProvider(); case "database": return new ConvexDataProvider(); case "hybrid": return new HybridDataProvider(); } } ``` ### 4. Component Refactoring **Update components to use data provider:** **`src/pages/blog/index.astro`:** ```astro --- import { getDataProvider } from '@/lib/data'; const dataProvider = getDataProvider(); const posts = await dataProvider.getBlogPosts(); --- <BlogSearch client:load posts={posts} /> ``` **`src/pages/blog/[...slug].astro`:** ```astro --- import { getDataProvider } from '@/lib/data'; import { FEATURES } from '@/config/features'; export async function getStaticPaths() { if (!FEATURES.STATIC_MODE) return []; const dataProvider = getDataProvider(); const posts = await dataProvider.getBlogPosts(); return posts.map((post) => ({ params: { slug: post.id }, props: { post }, })); } const { slug } = Astro.params; const dataProvider = getDataProvider(); const post = await dataProvider.getBlogPost(slug); --- ``` ### 5. Authentication Abstraction **Create conditional auth wrapper in `src/lib/auth/index.ts`:** ```typescript import { FEATURES } from "@/config/features"; export interface AuthProvider { getUser(): Promise<User | null>; signIn(provider: string): Promise<void>; signOut(): Promise<void>; } class NoAuthProvider implements AuthProvider { async getUser() { return null; } async signIn() { throw new Error("Auth disabled"); } async signOut() { throw new Error("Auth disabled"); } } class BetterAuthProvider implements AuthProvider { // Real implementation } export function getAuthProvider(): AuthProvider { return FEATURES.AUTH_ENABLED ? new BetterAuthProvider() : new NoAuthProvider(); } ``` ### 6. Conditional Component Loading **Create wrapper components:** **`src/components/ConditionalAuth.astro`:** ```astro --- import { FEATURES } from '@/config/features'; const { fallback } = Astro.props; --- {FEATURES.AUTH_ENABLED ? <slot /> : <Fragment>{fallback}</Fragment>} ``` **Usage:** ```astro <ConditionalAuth fallback={(<div>Login Disabled</div>)}> <LoginForm client:load /> </ConditionalAuth> ``` ### 7. Build Scripts **Add mode-specific build commands to `package.json`:** ```json { "scripts": { "dev": "astro dev", "dev:static": "STATIC_MODE=true astro dev", "dev:database": "CONVEX_ENABLED=true AUTH_ENABLED=true astro dev", "dev:hybrid": "CONVEX_ENABLED=true astro dev", "build": "astro build", "build:static": "STATIC_MODE=true astro build", "build:database": "CONVEX_ENABLED=true AUTH_ENABLED=true astro build", "build:hybrid": "CONVEX_ENABLED=true astro build" } } ``` ### 8. Type Safety **Conditional type exports:** ```typescript // src/types/index.ts import type { FEATURES } from "@/config/features"; export type ConditionalConvexTypes = typeof FEATURES.CONVEX_ENABLED extends true ? import("convex/react").ConvexReactClient : never; ``` ### 9. Dependency Management **Optional dependencies strategy:** - Keep Convex as regular dependency (tree-shaking will remove unused code) - Use dynamic imports: `await import('convex/react')` only when needed - Vite will optimize bundle based on dead code elimination **OR** for strict separation: ```json { "dependencies": { "astro": "^5.14.0" }, "optionalDependencies": { "convex": "^1.x.x", "better-auth": "^1.x.x" } } ``` ### 10. Migration Path **Step-by-step implementation:** 1. **Phase 1: Feature Flags** - Add environment variables - Create `src/config/features.ts` - No breaking changes 2. **Phase 2: Data Abstraction** - Create `src/lib/data/` directory - Implement provider interfaces - Add transformer utilities 3. **Phase 3: Component Updates** - Update blog pages to use data provider - Add conditional rendering - Test all three modes 4. **Phase 4: Auth Abstraction** - Create auth provider interface - Update auth components - Add conditional wrappers 5. **Phase 5: Build Configuration** - Update `astro.config.mjs` - Add build scripts - Update deployment workflows 6. **Phase 6: Documentation** - Update CLAUDE.md - Create mode-switching guide - Document environment variables ## Benefits **Zero Breaking Changes**: Existing code works as-is in database mode **Progressive Enhancement**: Start static, add database when needed **Performance**: Static mode = fastest possible site **Flexibility**: Switch modes with environment variables **Type Safety**: Full TypeScript support in all modes **Testing**: Easy to test without database dependencies **Cost**: Static mode = zero backend costs ## File Structure ``` src/ ├── config/ ├── features.ts # Feature flags & mode detection └── site.ts # Existing site config ├── lib/ ├── data/ ├── index.ts # Main data provider interface ├── static.ts # Content collection provider ├── convex.ts # Convex provider ├── hybrid.ts # Combined provider └── types.ts # Shared data types └── auth/ ├── index.ts # Auth provider interface ├── better-auth.ts # Better Auth implementation └── no-auth.ts # Disabled auth stub ├── components/ ├── ConditionalAuth.astro └── ConditionalFeature.astro └── pages/ └── blog/ ├── index.astro # Updated to use data provider └── [...slug].astro # Updated to use data provider ``` ## Testing Strategy 1. **Static Mode**: Run `bun build:static` and verify no Convex imports 2. **Database Mode**: Run `bun build:database` and test all Convex features 3. **Hybrid Mode**: Test content collection + database queries together 4. **Bundle Analysis**: Check bundle size in each mode ## Future Enhancements - Admin UI to switch modes at runtime - Content sync tool (markdown Convex) - Preview mode (database content in development) - A/B testing between modes - Gradual rollout (percentage of users see database content)