17 KiB
Indie — Independent Website Platform
A platform that lets anyone create, own, and publish their own static website without social media gatekeeping. Built with a drag-and-drop builder and LLM prompt-to-site generation. Every site is backed by a git repository. Users self-host or use free subdomain hosting.
Tech Stack
| Layer | Technology |
|---|---|
| Backend | Java 17, Spring Boot 3.x, Gradle (Groovy) |
| Frontend | Angular 17, Angular Material, Angular CDK (drag-drop) |
| Relational DB | PostgreSQL |
| Document DB | MongoDB (LLM sessions) |
| Object Storage | Cloudflare R2 (S3-compatible) |
| LLM | Gemini (primary), OpenAI (fallback), provider-abstraction for future |
| Auth | Spring Security + OAuth2 Client — email/password, Google OAuth, GitHub OAuth, OpenID Connect |
| Git | JGit — local repo per site, optional GitHub/GitLab remote push |
| Infra | Docker Compose (dev), R2 + Cloudflare (hosting), Caddy/nginx (prod reverse proxy) |
Build Phases
v0.1 — Auth & Foundation (Target: Weeks 1-3)
Goal: User can register, log in via email or OAuth, and create/manage sites and pages.
Backend
| File | Action | Details |
|---|---|---|
backend/build.gradle |
Add | Spring Boot 3.x, deps: web, jpa, security, oauth2-client, oauth2-resource-server, validation, lombok, postgresql, testcontainers, aws-java-sdk-s3 (R2), springdoc-openapi, jgit |
backend/settings.gradle |
Add | Project name, plugin management |
backend/src/main/resources/application.yml |
Add | DB configs, JWT keys, OAuth client IDs/secrets, LLM keys, R2 credentials, git config |
com/indie/IndieApplication.java |
Add | Main class |
com/indie/config/SecurityConfig.java |
Add | SecurityFilterChain with JWT filter + OAuth2 login + OpenID Connect. Route permissions: /api/auth/** public, /api/** authenticated |
com/indie/config/JwtConfig.java |
Add | RS256 signing, access token 15min, refresh token 7d |
com/indie/config/GitConfig.java |
Add | Repos root path (/var/git/sites/), author name/email |
com/indie/model/User.java |
Add | id(UUID), email, password(bcrypt, nullable), name, avatarUrl, provider(LOCAL/GOOGLE/GITHUB/OPENID), providerId, emailVerified, createdAt, updatedAt |
com/indie/model/Site.java |
Add | id, userId, name, description, subdomain(unique), themeConfig(JSONB), status(DRAFT/PUBLISHED), publishedAt, createdAt, updatedAt |
com/indie/model/Page.java |
Add | id, siteId(FK), title, slug, seoTitle, seoDescription, orderIndex, createdAt |
com/indie/model/Component.java |
Add | id, pageId(FK), type(enum), config(JSONB), styles(JSONB), orderIndex |
com/indie/repository/*.java |
Add | JPA repositories for User, Site, Page, Component |
com/indie/service/AuthService.java |
Add | register, login, refreshToken, oauthLogin |
com/indie/service/OAuth2UserService.java |
Add | Maps Google/GitHub/OpenID user info to User entity |
com/indie/controller/AuthController.java |
Add | POST /api/auth/register, /login, /refresh, /logout |
com/indie/controller/SiteController.java |
Add | Full CRUD: GET/POST /api/sites, GET/PUT/DELETE /api/sites/{id} |
com/indie/controller/PageController.java |
Add | CRUD for pages within a site |
Frontend
| File | Action | Details |
|---|---|---|
frontend/ |
Init | Angular 17 standalone, Angular Material, lazy routes, Tailwind CSS |
src/app/auth/ |
Add | LoginComponent, RegisterComponent, OAuth callback handler, AuthService, AuthGuard, JWT interceptor |
src/app/dashboard/ |
Add | DashboardComponent (list site cards), CreateSiteDialog, DeleteSiteDialog |
Infrastructure
| File | Action | Details |
|---|---|---|
docker-compose.yml |
Add | PostgreSQL, MongoDB, MinIO (local R2 mock), backend (8080), frontend (4200) |
v0.2 — Drag-and-Drop Builder (Target: Weeks 4-6)
Goal: User can visually build a website by dragging components onto a canvas, editing their properties, styling via theme editor, and previewing live.
Backend
| File | Action | Details |
|---|---|---|
com/indie/controller/BuilderController.java |
Add | GET /api/sites/{id}/pages (with nested components), PUT /api/sites/{id}/pages/{pageId}/components (batch save), GET/PUT /api/sites/{id}/theme |
com/indie/service/BuilderService.java |
Add | Validate component tree (type enum, config schema, max 50/page), save in transaction |
Frontend
| File | Action | Details |
|---|---|---|
src/app/builder/builder.component.ts |
Add | 3-panel layout (palette 250px, canvas flex, properties 320px) via CSS Grid or mat-sidenav |
src/app/builder/services/builder-state.service.ts |
Add | BehaviorSubject store: components$, selectedComponentId$, themeConfig$, pages$. Methods: add, remove, update, reorder. Undo/redo stack (50 snapshots) |
src/app/builder/services/auto-save.service.ts |
Add | 2s debounce → PUT batch save. "Saving..."/"Saved" indicator |
src/app/builder/component-palette/ |
Add | PaletteComponent — Material list of 10 draggable component types, organized by category |
src/app/builder/canvas/ |
Add | CanvasComponent — cdkDropList accepting drops, renders components in order, highlight on select, delete on hover |
src/app/builder/property-panel/ |
Add | PropertyPanelComponent — dynamic form per type (config + styles sections). Material expansion panels |
src/app/builder/renderer/ |
Add | Render components per type (heading, text, image, button, divider, gallery, video, contact_form, map, custom_html) — each takes config + styles as @Input() |
src/app/builder/theme-editor/ |
Add | ThemeEditorComponent — color pickers, font selectors, spacing slider. Live preview updates |
src/app/builder/preview/ |
Add | PreviewComponent — sandboxed <iframe> with srcdoc, responsive device toggles (desktop/tablet/mobile) |
src/app/builder/builder-api.service.ts |
Add | HTTP calls for save/load components, theme, pages |
Component types: heading, text, image, button, divider, gallery, video, contact_form, map, custom_html
v0.3 — LLM Prompt-to-Site (Target: Weeks 7-8)
Goal: User types a description of their desired website → AI generates the full site structure (pages + components + theme) → user tweaks in builder or refines via another prompt.
Backend
| File | Action | Details |
|---|---|---|
com/indie/service/llm/LLMProvider.java |
Add | Interface: generate(systemPrompt, userPrompt, options) → LLMResult. LLMResult: content, tokenUsage, model, latency |
com/indie/service/llm/GeminiProvider.java |
Add | Implements LLMProvider via Gemini API. Config: indie.llm.providers.gemini.api-key, model |
com/indie/service/llm/OpenAIProvider.java |
Add | Implements LLMProvider via OpenAI API. Config: indie.llm.providers.openai.api-key, model |
com/indie/service/llm/LLMFactory.java |
Add | Returns configured provider from indie.llm.active-provider, fallback chain |
com/indie/dto/LLMOptions.java |
Add | temperature, maxTokens, timeout, responseFormat |
com/indie/dto/SiteStructure.java |
Add | Top-level DTO: List<PageStructure> pages, ThemeConfig theme. Nested types for page and component |
com/indie/dto/LLMResult.java |
Add | content(String), tokenUsage, model, latencyMs |
com/indie/model/LLMSession.java |
Add | MongoDB document: userId, prompt, generatedStructure, acceptedAt, refinedPrompt, createdAt |
com/indie/service/LLMService.java |
Add | Orchestrator: prompt → LLM → parse → validate → return SiteStructure |
com/indie/service/LLMResponseParser.java |
Add | Parse LLM JSON output, handle markdown fences, malformed fields, partial extraction |
com/indie/controller/LLMController.java |
Add | POST /api/llm/generate (prompt→structure), POST /api/llm/refine (structure+prompt→refined). Rate limit: 10/hour/user |
com/indie/config/RateLimitConfig.java |
Add | Rate limiting via bucket4j |
src/main/resources/prompts/site-generator.txt |
Add | System prompt: schema definition + 2-3 few-shot examples |
Frontend
| File | Action | Details |
|---|---|---|
src/app/llm-workspace/ |
Add | LLMWorkspaceComponent — textarea, "Generate" button, loading spinner with ETA, result preview summary, "Apply to Builder" button |
src/app/llm-workspace/llm-session.service.ts |
Add | Prompt history, refinement flow (original prompt + user edits + refinement → new generation) |
v0.4 — Export, Git & Hosting (Target: Weeks 9-11)
Goal: User can publish their site as pure static files. Every publish creates a git commit. User can connect GitHub/GitLab for automatic pushes. Sites served at *.indie.com for free.
Backend
| File | Action | Details |
|---|---|---|
com/indie/service/RenderService.java |
Add | Core render engine: renderSite(site), renderPage(site, page, components) → String, renderCss(theme, components) → String, renderJs() → String |
com/indie/engine/templates/*.html |
Add | Thymeleaf templates per component type + layout.html + head.html |
com/indie/engine/StyleCompiler.java |
Add | Theme → CSS custom properties at :root, component styles → CSS classes, responsive breakpoints |
com/indie/engine/JsBundle.java |
Add | Generates script.js: nav toggle, smooth scroll, lightbox, contact form handler, comment form handler. Minified |
com/indie/service/ExportService.java |
Add | export(siteId) → ZipOutputStream — full site directory + .git/ |
com/indie/controller/ExportController.java |
Add | GET /api/sites/{id}/export → application/zip download |
com/indie/service/GitService.java |
Add | initRepo(siteId), commit(siteId, message), commitAndPush(siteId, message), getLog(siteId), rollback(siteId, commitHash), exportRepo(siteId) |
com/indie/model/GitRemote.java |
Add | JPA: id, siteId, provider(GITHUB/GITLAB), remoteUrl, encryptedToken, defaultBranch |
com/indie/service/KeyManager.java |
Add | SSH keypair generation per site, encrypted storage |
com/indie/controller/GitController.java |
Add | POST /api/sites/{id}/git/connect, /disconnect, GET /api/sites/{id}/git/status |
com/indie/service/DeploymentService.java |
Add | On publish: render → write to git working dir → GitService.commit → upload to R2 → return public URL |
com/indie/controller/DeploymentController.java |
Add | POST /api/sites/{id}/publish, /unpublish |
com/indie/service/DomainService.java |
Add | Skeleton: store custom domain mapping in Site entity, prepared for future TLS |
com/indie/service/MediaService.java |
Add | Upload to R2, validate type/size, resize |
com/indie/controller/MediaController.java |
Add | POST /api/media/upload, GET /api/media/{id} |
Frontend
| File | Action | Details |
|---|---|---|
src/app/builder/git-settings/ |
Add | GitSettingsComponent — connect/disconnect GitHub/GitLab OAuth, select remote repo, connection status |
src/app/builder/settings/ |
Add | SettingsComponent — site name, subdomain, export zip button, publish/unpublish buttons |
Infrastructure
| File | Action | Details |
|---|---|---|
infra/cloudflare-worker.js |
Add | Routes *.indie.com → R2 bucket indie-sites/{subdomain}/ |
docker-compose.prod.yml |
Add | Production services: PostgreSQL, MongoDB, backend (×2), nginx, certbot |
Git directory structure on server:
/var/git/sites/
└── {siteId}/
├── .git/
├── index.html
├── about/index.html
├── style.css
├── script.js
└── assets/
v0.5 — Social Features & Polish (Target: Weeks 12-13)
Goal: Minimal social features (comments, RSS) and final polish before public release.
Backend
| File | Action | Details |
|---|---|---|
com/indie/model/Comment.java |
Add | JPA: id(UUID), siteId, pageId, authorName, authorEmail, content, isApproved, parentCommentId, createdAt |
com/indie/controller/CommentController.java |
Add | Public: POST /api/sites/{subdomain}/comments. Authenticated: GET/PUT/DELETE moderation |
com/indie/service/RssService.java |
Add | generateRss(siteId) → String — RSS 2.0 feed from site pages |
com/indie/controller/RssController.java |
Add | GET /api/feed/{subdomain} → application/rss+xml, 1h cache |
Frontend
| File | Action | Details |
|---|---|---|
src/app/builder/settings/ |
Modify | Add git history panel (commit list, rollback button, download repo) |
src/app/builder/components/comments-section/ |
Add | In-builder config (enable/disable, moderate) + renderer for generated sites |
Infrastructure Architecture
┌──────────────────────┐
│ Cloudflare CDN │
│ *.indie.com → R2 │
└────────┬─────────────┘
│
┌────────▼─────────────┐
│ Cloudflare R2 │
│ indie-sites bucket │
│ /{subdomain}/* │
└──────────────────────┘
▲
┌────────┴─────────────┐
│ Spring Boot API │
│ (VPS / Docker) │
└────────┬─────────────┘
│
┌──────────────┼──────────────┐
│ │ │
┌────────▼───┐ ┌──────▼──────┐ ┌───▼────────┐
│ PostgreSQL │ │ MongoDB │ │ Git Repos │
│ (users, │ │ (LLM sess.) │ │ /var/git/ │
│ sites, │ └─────────────┘ └────────────┘
│ pages, │
│ comps) │
└─────────────┘
LLM Provider Abstraction
// Adding a new provider = implement this interface + add config
public interface LLMProvider {
LLMResult generate(String systemPrompt, String userPrompt, LLMOptions options);
String getName();
}
// application.yml
indie:
llm:
active-provider: gemini
fallback-provider: openai
providers:
gemini:
api-key: ${GEMINI_API_KEY}
model: gemini-2.0-flash
openai:
api-key: ${OPENAI_API_KEY}
model: gpt-4o-mini
Key Design Decisions
-
Static output only — Generated sites are pure HTML/CSS/JS. No Angular/React dependency for end users. They own the output forever, no lock-in.
-
Git as source of truth — Every site is a git repository. Publish = commit. Rollback is a
git checkout. Export includes full.git/history. -
Shared rendering — The same component renderers drive the builder preview AND the static export. What you see is what you get.
-
No billing in MVP — Subdomain hosting is free. No Stripe/PayPal code. Monetization (paid hosting, custom domains) is a post-MVP concern.
-
Incremental releases — Each v0.x is independently shippable. If timeline slips, ship what's done.
Validation Strategy
| Release | What to Test |
|---|---|
| v0.1 | Register, login (email + OAuth), refresh token, site CRUD, page CRUD, 401 on unauthenticated |
| v0.2 | Drag → reorder → save → reload matches, theme change reflects in preview, undo/redo, all 10 component types render |
| v0.3 | LLM generates valid SiteStructure JSON, generation loads into builder, refinement updates correctly, error handling (timeout, bad JSON) |
| v0.4 | Exported zip opens in browser identically to preview, git init + commit works, git push to remote works, R2 upload succeeds, subdomain serves content |
| v0.5 | Comment submit → approve → display, RSS validates, image upload + resize, rollback restores previous version |
Risk Register
| Risk | Likelihood | Mitigation |
|---|---|---|
| Scope creep (adding features mid-phase) | High | Strict feature freeze per v0.x; ship as-is |
| Drag-drop performance with many components | Medium | Cap at 50 components/page; virtual scroll if needed |
| LLM output quality inconsistent | Medium | Iterate on system prompt; template gallery fallback |
| Solo developer burnout | Medium | Realistic timeline; celebrate each release |
| LLM API costs | Low | Gemini-2.0-flash is free-tier friendly; rate limit 10/hour/user |