# Indie — Independent Website Platform A platform that lets anyone create, own, and publish their own static website through AI prompts. Describe what you want, and the AI generates it. Every site is backed by a git repository. --- ## Quick Start ### Prerequisites - Java 17+ - Node.js 18+ and npm - Docker & Docker Compose (for PostgreSQL, MongoDB, MinIO) - npm packages: `@angular/cli@17` (`npm install -g @angular/cli@17`) ### Setup 1. **Start infrastructure:** ```bash docker compose up -d ``` This starts PostgreSQL (port 5432), MongoDB (port 27017), and MinIO (ports 9000/9001). 2. **Start the backend:** ```bash cd backend ./gradlew bootRun ``` The API will be available at `http://localhost:8080`. Swagger UI at `http://localhost:8080/swagger-ui.html`. 3. **Start the frontend (separate terminal):** ```bash cd frontend npm install ng serve ``` The app will be available at `http://localhost:4200`. API calls are proxied to `http://localhost:8080`. --- ## Infrastructure (Docker Compose) | Service | Image | Ports | Credentials | Volume | |------------|--------------------|--------------|--------------------------------|---------------------| | PostgreSQL | `postgres:16-alpine` | host:5432 | `indie` / `indie_dev` | `pgdata` | | MongoDB | `mongo:7` | host:27017 | (no auth) | `mongodata` | | MinIO | `minio/minio:latest` | host:9000 (S3 API), host:9001 (Console) | `indieadmin` / `indiesecret` | `miniodata` | MinIO mimics Cloudflare R2/S3 for local development. Console: `http://localhost:9001`. --- ## Environment Variables | Variable | Description | Default | |----------|-------------|---------| | **Database** | | | | `SPRING_DATASOURCE_URL` | PostgreSQL JDBC URL | `jdbc:postgresql://localhost:5432/indie` | | `SPRING_DATASOURCE_USERNAME` | DB username | `indie` | | `SPRING_DATASOURCE_PASSWORD` | DB password | `indie_dev` | | `MONGODB_URI` | MongoDB connection URI | `mongodb://localhost:27017/indie` | | **JWT** | | | | `INDIE_JWT_ACCESS_EXPIRATION` | Access token TTL (ms) | `900000` (15 min) | | `INDIE_JWT_REFRESH_EXPIRATION` | Refresh token TTL (ms) | `604800000` (7 days) | | `INDIE_JWT_PRIVATE_KEY_PATH` | RS256 private key path | `config/jwt-private.key` | | `INDIE_JWT_PUBLIC_KEY_PATH` | RS256 public key path | `config/jwt-public.key` | | **Git** | | | | `INDIE_GIT_REPOS_PATH` | Git repositories root | `/var/git/sites/` | | `INDIE_GIT_AUTHOR_NAME` | Default git author name | `Indie Platform` | | `INDIE_GIT_AUTHOR_EMAIL` | Default git author email | `git@indie.local` | | **Storage (S3-compatible)** | | | | `INDIE_STORAGE_ENDPOINT` | S3 endpoint URL | `http://localhost:9000` | | `INDIE_STORAGE_REGION` | S3 region | `auto` | | `INDIE_STORAGE_ACCESS_KEY` | S3 access key | `indie` | | `INDIE_STORAGE_SECRET_KEY` | S3 secret key | `indie_dev` | | `INDIE_STORAGE_BUCKET` | S3 bucket name | `indie-sites` | | `INDIE_STORAGE_PUBLIC_URL` | Public URL base for stored files | `http://localhost:9000/indie-sites` | | **Encryption** | | | | `INDIE_ENCRYPTION_KEY_PATH` | AES-256-GCM master key path | `config/encryption.key` | | **App** | | | | `INDIE_APP_BASE_URL` | Frontend base URL (for verification emails) | `http://localhost:4200` | | `INDIE_VERIFICATION_TOKEN_EXPIRATION` | Email verification token TTL (minutes) | `1440` (24h) | | `INDIE_CORS_ORIGINS` | Allowed CORS origins (comma-separated) | `http://localhost:4200,http://localhost:3000` | | **LLM** | | | | `GEMINI_API_KEY` | Google Gemini API key | — | | `GEMINI_MODEL` | Gemini model name | `gemini-2.0-flash` | | `OPENAI_API_KEY` | OpenAI API key | — | | `OPENAI_MODEL` | OpenAI model name | `gpt-4o-mini` | | `INDIE_LLM_RATE_LIMIT` | Max LLM requests per hour per user | `10` | | **Email (MailHog compatible)** | | | | `EMAIL_HOST` | SMTP host | `localhost` | | `EMAIL_PORT` | SMTP port | `1025` | | `EMAIL_USERNAME` | SMTP username | _(empty)_ | | `EMAIL_PASSWORD` | SMTP password | _(empty)_ | | `EMAIL_SMTP_AUTH` | Enable SMTP auth | `true` | | `EMAIL_SMTP_STARTTLS` | Enable STARTTLS | `true` | --- ## Project Structure ``` backend/ ├── build.gradle # Dependencies and build config ├── settings.gradle # Project name ├── gradlew / gradlew.bat # Gradle wrapper ├── gradle/wrapper/ # Wrapper JAR + properties └── src/ ├── main/java/com/krrishg/ │ ├── IndieApplication.java # Spring Boot main class (@EnableScheduling) │ ├── config/ │ │ ├── SecurityConfig.java # HTTP security, JWT filter, CORS │ │ ├── JwtConfig.java # RS256 token generation and validation │ │ ├── TokenService.java # Interface for token generation/validation │ │ ├── JwtAuthenticationToken.java # Custom AbstractAuthenticationToken │ │ ├── UserPrincipal.java # UserDetails record │ │ ├── GitConfig.java # Git repository settings │ │ ├── StorageConfig.java # S3Client bean (MinIO / S3-compatible) │ │ ├── EncryptionService.java # AES-256/GCM for LLM API key encryption │ │ ├── RateLimitConfig.java # Bucket4j token-bucket rate limiter │ │ ├── SchemaMigration.java # Startup DDL migrations │ │ ├── MapToJsonConverter.java # JPA converter for JSON text columns │ │ └── GlobalExceptionHandler.java # @ControllerAdvice error handling │ ├── model/ │ │ ├── User.java # User entity (email, password, emailVerified) │ │ ├── VerificationToken.java # Email verification / set-password tokens │ │ ├── Site.java # Site entity per user │ │ ├── SiteStatus.java # Enum: DRAFT, PUBLISHED │ │ ├── AuthProvider.java # Enum: LOCAL only (OAuth planned) │ │ ├── GitRemote.java # Connected git remote (GitHub/GitLab) │ │ ├── SelfHostedConfig.java # SSH/deploy configuration per site │ │ └── GenerationVersion.java # MongoDB: LLM generation history per site │ ├── repository/ │ │ ├── UserRepository.java │ │ ├── SiteRepository.java │ │ ├── GitRemoteRepository.java │ │ ├── SelfHostedConfigRepository.java │ │ └── VerificationTokenRepository.java │ ├── dto/ │ │ ├── RegisterRequest.java │ │ ├── RegistrationResponse.java │ │ ├── LoginRequest.java │ │ ├── RefreshTokenRequest.java │ │ ├── AuthResponse.java # JWT tokens + user info │ │ ├── VerifyEmailRequest.java │ │ ├── VerifyEmailResponse.java │ │ ├── ResendVerificationRequest.java │ │ ├── SetPasswordRequest.java │ │ ├── SiteRequest.java # Includes deploymentTarget field │ │ ├── SiteResponse.java │ │ ├── SiteStructure.java # Top-level DTO: pages + theme │ │ ├── Reference.java # URL/IMAGE reference for LLM context │ │ ├── LLMOptions.java # LLM generation options │ │ ├── LLMResult.java # LLM generation result │ │ ├── LLMConfigRequest.java # User LLM provider config │ │ ├── SelfHostedConfigRequest.java │ │ └── SelfHostedConfigResponse.java │ ├── service/ │ │ ├── AuthService.java # Register, login, token refresh, verify email │ │ ├── EmailService.java # Verification email sender │ │ ├── CleanupService.java # Scheduled cleanup of stale unverified accounts │ │ ├── LLMService.java # Orchestrates LLM calls, stores/restores versions │ │ ├── LLMResponseParser.java # Parses LLM JSON into SiteStructure │ │ ├── ReferenceService.java # Fetches web page content for LLM context │ │ ├── RenderService.java # Renders SiteStructure into HTML + CSS + JS │ │ ├── DeploymentService.java # Orchestrates publish/unpublish │ │ ├── Deployer.java # Interface for self-hosted deployment │ │ ├── SshRsyncDeployer.java # SSH + rsync + nginx + Let's Encrypt │ │ ├── PagesProvider.java # Interface for git provider Pages │ │ ├── GitHubPagesProvider.java # GitHub Pages implementation │ │ ├── GitLabPagesProvider.java # GitLab Pages implementation │ │ ├── GitService.java # JGit: init, commit, push, log, rollback │ │ ├── KeyManager.java # SSH key-pair generation per site │ │ ├── ExportService.java # Exports site structure to ZIP │ │ └── DomainService.java # Custom domain resolution skeleton │ ├── service/llm/ │ │ ├── LLMProvider.java # Interface for LLM providers │ │ ├── GeminiProvider.java # Google Gemini implementation │ │ ├── OpenAIProvider.java # OpenAI implementation │ │ └── ProviderFactory.java # Factory creating providers by name + key │ ├── engine/ │ │ ├── JsBundle.java # Generates static JS (nav, smooth scroll, lightbox, contact form) │ │ └── StyleCompiler.java # Generates base CSS with custom property fallbacks │ └── controller/ │ ├── AuthController.java # POST /api/auth/* │ ├── SiteController.java # CRUD /api/sites │ ├── LLMController.java # POST /api/llm/generate, /refine, /versions/* │ ├── GitController.java # POST /api/sites/{id}/git/* │ ├── DeploymentController.java # POST /api/sites/{id}/publish, /unpublish │ ├── ExportController.java # GET /api/sites/{id}/export │ ├── UserSettingsController.java # GET/PUT/DELETE /api/user/llm-config │ └── SelfHostedController.java # CRUD /api/sites/{id}/self-hosted ├── main/resources/ │ ├── application.yml # All configuration │ └── prompts/ │ ├── site-generator.txt # LLM system prompt for site generation │ └── site-refiner.txt # LLM system prompt for refinement └── test/java/com/krrishg/ ├── config/ # 7 config tests ├── controller/ # 9 controller tests ├── dto/ # 7 DTO tests ├── engine/ # 2 engine tests ├── model/ # 7 model tests ├── repository/ # 4 repository tests ├── service/ # 16 service tests + fakes ├── service/llm/ # 3 LLM provider tests └── support/ # 13 test fakes, stubs, and configs frontend/ ├── Dockerfile # Multi-stage production build ├── nginx.conf # Nginx config (SPA fallback + /api/ proxy) ├── proxy.conf.json # Dev proxy to backend ├── angular.json # Angular CLI configuration ├── tailwind.config.js # Tailwind CSS configuration ├── postcss.config.js # PostCSS (tailwindcss + autoprefixer) ├── package.json ├── tsconfig.json / tsconfig.app.json / tsconfig.spec.json ├── src/ │ ├── index.html │ ├── main.ts # Angular bootstrap │ ├── styles.css # Global styles + Tailwind imports │ ├── favicon.ico │ ├── assets/ │ └── app/ │ ├── app.component.ts / .html / .css │ ├── app.config.ts # App providers │ ├── app.routes.ts # Lazy-loaded routes │ ├── core/ │ │ ├── models/ │ │ │ ├── auth-response.ts # AuthResponse & UserInfo │ │ │ ├── site.ts # SiteResponse │ │ │ ├── page.ts # PageResponse │ │ │ ├── llm.ts # LLM request/response types, GenerationVersion │ │ │ └── deployment.ts # Git/self-hosted config types │ │ ├── services/ │ │ │ ├── auth.service.ts # Auth HTTP + token storage │ │ │ ├── site.service.ts # Site CRUD HTTP │ │ │ ├── page.service.ts # Page CRUD HTTP │ │ │ ├── deployment.service.ts # Export, git, publish HTTP │ │ │ ├── llm-api.service.ts # LLM generate/refine/version HTTP │ │ │ └── llm-config.service.ts # User LLM key storage HTTP │ │ ├── guards/ │ │ │ ├── auth.guard.ts # Redirects unauthenticated to /login │ │ │ └── login.guard.ts # Redirects authenticated to /dashboard │ │ └── interceptors/ │ │ └── jwt.interceptor.ts # Attaches Bearer token │ ├── auth/ │ │ ├── login/ │ │ │ └── login.component.ts │ │ ├── register/ │ │ │ └── register.component.ts │ │ └── verify-email/ │ │ └── verify-email.component.ts # Email verification + set-password │ ├── dashboard/ │ │ ├── dashboard.component.ts # Site cards grid + toolbar │ │ ├── create-site-dialog/ │ │ │ └── create-site-dialog.ts │ │ └── delete-site-dialog/ │ │ └── delete-site-dialog.ts │ └── llm-workspace/ │ ├── llm-workspace.component.ts # Main workspace (prompt, preview, timeline) │ ├── llm-session.service.ts # LLM generation session state │ ├── settings/ │ │ └── settings.component.ts # LLM provider key/dialog settings │ ├── git-settings/ │ │ └── git-settings.component.ts # Git remote connection settings │ ├── preview-dialog/ │ │ └── preview-dialog.component.ts # Preview generated site │ └── delete-version-dialog/ │ └── delete-version-dialog.ts # Confirm delete version config/ # Auto-generated on first boot ├── jwt-private.key # RS256 private key ├── jwt-public.key # RS256 public key └── encryption.key # AES-256-GCM master key (base64) ``` --- ## API Overview ### Authentication (`/api/auth`) | Method | Path | Description | |--------|------|-------------| | POST | `/api/auth/register` | Register with name + email + password (returns verification pending) | | POST | `/api/auth/login` | Login with email + password | | POST | `/api/auth/refresh` | Refresh access token | | POST | `/api/auth/logout` | Logout | | POST | `/api/auth/verify-email` | Verify email via token, returns setup token | | POST | `/api/auth/resend-verification` | Resend verification email | | POST | `/api/auth/set-password` | Set password after email verification | ### Sites (`/api/sites`) | Method | Path | Description | |--------|------|-------------| | GET | `/api/sites` | List user's sites | | POST | `/api/sites` | Create a new site (with `deploymentTarget`: `GIT_PAGES`, `SELF_HOSTED`, or `NONE`) | | GET | `/api/sites/{id}` | Get site details | | PUT | `/api/sites/{id}` | Update site | | DELETE | `/api/sites/{id}` | Delete site | ### LLM Generation (`/api/llm`) | Method | Path | Description | |--------|------|-------------| | POST | `/api/llm/generate` | Generate site from prompt (rate-limited to 10/hour/user) | | POST | `/api/llm/refine` | Refine existing site structure with follow-up prompt | | GET | `/api/llm/versions/{siteId}` | List generation versions for a site | | POST | `/api/llm/versions/{id}/restore` | Restore a specific generation version | | DELETE | `/api/llm/versions/{id}` | Delete a generation version | ### Git Remotes (`/api/sites/{id}/git`) | Method | Path | Description | |--------|------|-------------| | POST | `/api/sites/{id}/git/connect` | Connect git remote (GitHub/GitLab) | | POST | `/api/sites/{id}/git/disconnect` | Disconnect git remote | | GET | `/api/sites/{id}/git/status` | Get connection status, SSH keys, commit log | ### Deployment (`/api/sites/{id}`) | Method | Path | Description | |--------|------|-------------| | POST | `/api/sites/{id}/publish` | Publish site (git push + S3 upload) | | POST | `/api/sites/{id}/unpublish` | Unpublish site | | GET | `/api/sites/{id}/export` | Download site as `.zip` archive | ### Self-Hosted Deployment (`/api/sites/{id}/self-hosted`) | Method | Path | Description | |--------|------|-------------| | GET | `/api/sites/{id}/self-hosted` | Get SSH/deploy configuration | | PUT | `/api/sites/{id}/self-hosted` | Save SSH/deploy configuration | | DELETE | `/api/sites/{id}/self-hosted` | Remove configuration | ### User Settings (`/api/user`) | Method | Path | Description | |--------|------|-------------| | GET | `/api/user/llm-config` | Get saved LLM provider configs (keys redacted) | | PUT | `/api/user/llm-config` | Save API key / base URL / model for a provider | | DELETE | `/api/user/llm-config/{provider}` | Delete saved config for a provider | --- ## Frontend Routes | Path | Component | Auth | |------|-----------|------| | `/login` | LoginComponent | Redirects to /dashboard if already logged in | | `/register` | RegisterComponent | Redirects to /dashboard if already logged in | | `/verify-email` | VerifyEmailComponent | Public (email verification flow) | | `/dashboard` | DashboardComponent | Requires auth | | `/llm/:siteId` | LlmWorkspaceComponent | Requires auth | | `/llm/:siteId/settings` | SettingsComponent | Requires auth | | `/llm/:siteId/git` | GitSettingsComponent | Requires auth | --- ## Tech Stack | Layer | Technology | |-------|-----------| | Backend | Java 17, Spring Boot 3.2, Gradle (Groovy) | | Frontend | Angular 17, Angular Material, Tailwind CSS, Leaflet | | Relational DB | PostgreSQL 16 | | Document DB | MongoDB 7 (generation versions, LLM sessions) | | Object Storage | MinIO (dev) / Cloudflare R2 (prod) | | Auth | Spring Security + JWT (RS256), email/password | | LLM | Gemini (primary), OpenAI (fallback), provider-abstraction | | Git | JGit — local repo per site, GitHub/GitLab remote push | | Email | Spring Boot Mail (MailHog compatible) | | Rate Limiting | Bucket4j | | API Docs | Springdoc OpenAPI (Swagger UI) | --- ## Architecture ``` ┌──────────────────────┐ │ Cloudflare CDN │ │ custom domains → R2 │ └────────┬─────────────┘ │ ┌────────▼─────────────┐ │ Cloudflare R2 │ │ indie-sites bucket │ │ /{siteId}/* │ └──────────────────────┘ ▲ ┌────────┴─────────────┐ │ Spring Boot API │ │ (VPS / Docker) │ └────────┬─────────────┘ │ ┌──────────────┼──────────────┐ │ │ │ ┌────────▼───┐ ┌──────▼──────┐ ┌───▼────────┐ │ PostgreSQL │ │ MongoDB │ │ Git Repos │ │ (users, │ │ (versions, │ │ /var/git/ │ │ sites) │ │ sessions) │ └────────────┘ └─────────────┘ └─────────────┘ ``` --- ## Development ```bash # Backend cd backend ./gradlew build # Build ./gradlew test # Run tests (unit + slice + integration) ./gradlew bootRun # Run locally (requires docker compose up -d first) # Frontend cd frontend npm install # Install dependencies ng serve # Dev server at localhost:4200 ng build # Production build (outputs to dist/) ng test # Run unit tests (Jasmine + Karma) ``` ### Production Build (Docker) ```bash docker build -t indie-frontend frontend/ docker run -p 80:80 indie-frontend ``` The frontend Docker image serves the app via nginx (port 80) and proxies `/api/` requests to `http://backend:8080`. --- ## Roadmap ### v0.1 — Auth & Foundation ✅ - [x] User registration (email/password + email verification) - [x] User login (email/password + JWT) - [x] Token refresh with 7-day refresh token - [x] Site CRUD (create, list, get, update, delete) - [x] Frontend: Login, Register, Verify Email, Dashboard - [x] Frontend: JWT interceptor, auth guard, login guard, lazy routes - [x] Scheduled cleanup of unverified accounts ### v0.2 — LLM Prompt Workspace ✅ - [x] Prompt-to-site generation via Gemini/OpenAI - [x] Version timeline with MongoDB storage - [x] Live preview (sandboxed iframe) - [x] Content editor (grouped by page) - [x] Refine via follow-up prompts - [x] Rate limiting (10 generations/hour/user) - [x] Reference URL support for context ### v0.3 — Export, Git & Hosting ✅ - [x] Render engine (HTML + CSS + JS generation) - [x] Git init, commit, push via JGit - [x] SSH key-pair management per site - [x] GitHub Pages / GitLab Pages providers - [x] Export as `.zip` archive - [x] Publish/unpublish flow - [x] Custom domain skeleton - [x] Self-hosted deployment (SSH + rsync + nginx) --- ## LLM Provider Abstraction ```java public interface LLMProvider { LLMResult generate(String systemPrompt, String userPrompt, List references, LLMOptions options); String getName(); } ``` Adding a new provider = implement `LLMProvider` interface + add config block in `application.yml`. Provider API keys can be configured globally via env vars (`GEMINI_API_KEY`, `OPENAI_API_KEY`) or per-user through the `/api/user/llm-config` endpoint (encrypted at rest via AES-256-GCM). --- ## Key Design Decisions 1. **Pure prompt-based** — No drag-and-drop builder. Every site starts from a prompt. Iteration happens through follow-up prompts and inline text edits. 2. **Static output only** — Generated sites are pure HTML/CSS/JS. No runtime framework dependency. Users own the output forever. 3. **Git as source of truth** — Every site is a git repository. Publish = commit. Rollback = checkout. Export includes full `.git/`. 4. **Version timeline** — Every generation creates a version in MongoDB. Users can browse, preview, and restore any previous version. 5. **No billing in MVP** — Monetization is post-MVP. --- ## config/ Directory The following files live in `config/` and are auto-generated on first boot if they don't exist. In production, generate and mount them manually: ```bash # RS256 key pair for JWT openssl genpkey -algorithm RSA -out config/jwt-private.key -pkeyopt rsa_keygen_bits:2048 openssl rsa -pubout -in config/jwt-private.key -out config/jwt-public.key # AES-256-GCM master key for LLM API key encryption openssl rand -base64 32 > config/encryption.key ``` Set the `INDIE_JWT_PRIVATE_KEY_PATH`, `INDIE_JWT_PUBLIC_KEY_PATH`, and `INDIE_ENCRYPTION_KEY_PATH` env vars to point to these files.