freakfy Svelte Themes

Freakfy

A self-hosted static music player for anime soundtracks, built with Astro islands, Svelte, React and TailwindCSS.

🎵 FreakFy

Un clon de Spotify construido con Astro, Svelte, React y Tailwind, conectado a un backend Go, enfocado en organizar y reproducir música de tus animes favoritos.


📖 Descripción

FreakFy nació como un clon personal de Spotify basado en el excelente tutorial de @midudev, y se convirtió en un espacio propio donde catalogar los openings, endings y OSTs de los animes favoritos del autor.

El frontend sigue Screaming Architecture por features (src/features/), consume un backend Go (hexagonal, CQRS, PostgreSQL 16, Redis 7) a través del proxy de nginx en la misma origin, y se despliega como sitio estático sobre un VPS propio con Dokploy.

Refactor completado (2024): El backend Go fue refactorizado para sigue una arquitectura hexagonal con dominio/persistencia desacoplado, manejo centralizado de errores, middleware de hardening (request-id, body-limit, timeouts), y tests de contrato para compatibilidad con el frontend.


📚 Tabla de contenidos


🏛️ Arquitectura

FreakFy es un sitio estático Astro (sin SSR) que se conecta a un backend Go en la misma origin gracias a un proxy nginx. El frontend sigue el patrón de islas: HTML pre-renderizado en build, con partes interactivas hidratadas en cliente con React o Svelte.

Arquitectura Completa — Frontend + Backend + Deployment

%%{init: {
  "theme": "base",
  "themeVariables": {
    "primaryColor": "#229bc1",
    "primaryTextColor": "#ffffff",
    "primaryBorderColor": "#229bc1",
    "lineColor": "#229bc1",
    "secondaryColor": "#181818",
    "tertiaryColor": "#282828"
  }
}}%%
flowchart TB
  subgraph dev["💻 Desarrollo Local"]
    direction TB
    PC[PC Desarrollador]
    Vite[Vite Dev Server<br/>:4321]
    GoAPI[Go API<br/>localhost:8080]
    DB[(PostgreSQL<br/>localhost:5432)]
    Cache[(Redis<br/>localhost:6379)]

    PC -->|pnpm dev| Vite
    Vite -->|proxy /api/*| GoAPI
    GoAPI --> DB
    GoAPI --> Cache
  end

  subgraph build["🏗️ Build Time"]
    Vite -->|pnpm build| Astro[Astro SSG]
    Script[fetch-playlists.mjs] -->|genera| PlaylistIDs[playlist-ids.json]
    PlaylistIDs --> Astro
    Astro -->|output| Dist[dist/ HTML]
  end

  subgraph prod["🚀 Producción VPS"]
    direction TB
    nginx[nginx:alpine<br/>freakfy-web]
    GoProd[Go + Gin<br/>freakfy-api]
    Traefik[Traefik + TLS<br/>dokploy]
    User[👤 Usuario]
    DBprod[(PostgreSQL)]
    CacheProd[(Redis)]
    Worker[Go + Asynq<br/>freakfy-worker]

    Traefik -->|https| User
    User -->|http| nginx
    nginx -->|serve static| Dist2[dist/ HTML]
    nginx -->|proxy /api/*| GoProd
    GoProd --> DBprod
    GoProd --> CacheProd
    GoProd -->|async jobs| Worker
  end

  Script -.->|se genera en build| Dist2

Diagrama 1: Arquitectura Backend Go (Hexagonal + CQRS)

%%{init: {
  "theme": "base",
  "themeVariables": {
    "primaryColor": "#00add8",
    "primaryTextColor": "#ffffff",
    "primaryBorderColor": "#00add8",
    "lineColor": "#00add8",
    "secondaryColor": "#1a1a2e",
    "tertiaryColor": "#16213e"
  }
}}%%
flowchart TB
  subgraph client["🌐 Clientes"]
    Browser[Browsers]
    Mobile[Mobile Apps]
    ThirdParty[3rd Party]
  end

  subgraph gateway["🚪 API Gateway Layer"]
    Nginx[nginx:alpine<br/>Proxy]
    Gin[Gin Router<br/>/api/v1/*]
    Middleware[Middleware Stack<br/>- RequestID<br/>- Auth<br/>- CORS<br/>- RateLimit<br/>- Cache<br/>- Sanitize<br/>- Timing]
  end

  subgraph handlers["🎯 HTTP Handlers"]
    AuthH[auth_handlers.go<br/>- Register<br/>- Login<br/>- Refresh]
   PlaylistH[playlist_handlers.go<br/>- GetPlaylists<br/>- Create<br/>- Update<br/>- Delete<br/>- AddSong<br/>- RemoveSong]
    SongH[song_handlers.go<br/>- GetSongs<br/>- Search]
    UserH[user_handlers.go<br/>- GetMe<br/>- GetFavorites<br/>- ToggleFavorite<br/>- History]
  end

  subgraph cqrs["⚡ CQRS Layer"]
    subgraph commands["✍️ Commands (Write)"]
      CmdHandler[Command Handler<br/>cmd_handler.go]
      Cmd1[RegisterUser]
      Cmd2[CreatePlaylist]
      Cmd3[AddSongToPlaylist]
      Cmd4[ToggleFavorite]
      Cmd5[RecordHistory]
    end

    subgraph queries["📖 Queries (Read)"]
      QueryHandler[Query Handler<br/>query_handler.go]
      Q1[GetUserPlaylists]
      Q2[GetPlaylistByID]
      Q3[SearchSongs]
      Q4[GetUserFavorites]
      Q5[GetHistory]
    end

    ReadFacade[Read Facade<br/>- Playlist + Songs join<br/>- Pagination]
  end

  subgraph domain["🏛️ Domain Layer"]
    Ent[Entities<br/>- User<br/>- Playlist<br/>- Song<br/>- Session<br/>- Favorite<br/>- History]
    Services[Domain Services<br/>- AuthService<br/>- PlaylistService<br/>- SongService]
    Repos[Repository Interfaces<br/>- UserRepository<br/>- PlaylistRepository<br/>- SongRepository<br/>...]
    VO[Value Objects<br/>- Pagination<br/>- Password<br/>- Email<br/>- Color]
  end

  subgraph infra["🏗️ Infrastructure Layer"]
    GORM[GORM Implementation]
    Models[SQL Models<br/>- UserModel<br/>- PlaylistModel<br/>- SongModel<br/>...]
    Mappers[Entity ↔ Model<br/>Mappers]
    DB[(PostgreSQL 16<br/>:5432)]
    Redis[(Redis 7<br/>:6379)]
    Asynq[Asynq Worker<br/>Background Jobs]
  end

  subgraph pkg["📦 Paquetes Compartidos"]
    Config[config/<br/>- Load/env<br/>- Validate]
    Error[apierror/<br/>- Error codes<br/>- FromContext]
    Response[response/<br/>- OK/Error<br/>- Paginated]
    Security[security/<br/>- JWT<br/>- Hash]
    Logger[logger/<br/>- Structured JSON]
    Metrics[metrics/<br/>- Histograms<br/>- Stats]
    Cache[caching/<br/>- Query Cache<br/>- ETag]
    Retry[retry/<br/>- Backoff<br/>- Circuit Breaker]
  end

  client -->|HTTP| Nginx
  Nginx -->|route| Gin
  Gin -->|middleware chain| Middleware
  Middleware -->|dispatch| AuthH
  Middleware -->|dispatch| PlaylistH
  Middleware -->|dispatch| SongH
  Middleware -->|dispatch| UserH

  AuthH --> CmdHandler
  PlaylistH -->|CQ| CmdHandler
  PlaylistH -->|CQ| QueryHandler
  SongH -->|CQ| QueryHandler
  UserH -->|CQ| CmdHandler
  UserH -->|CQ| QueryHandler

  CmdHandler -->|execute| Cmd1
  CmdHandler -->|execute| Cmd2
  CmdHandler -->|execute| Cmd3
  CmdHandler -->|execute| Cmd4
  CmdHandler -->|execute| Cmd5

  QueryHandler -->|execute| Q1
  QueryHandler -->|execute| Q2
  QueryHandler -->|execute| Q3
  QueryHandler -->|execute| Q4
  QueryHandler -->|execute| Q5

  Q1 --> ReadFacade
  Q2 --> ReadFacade

  Cmd1 -->|persists| Services
  Cmd2 -->|persists| Services
  Cmd3 -->|persists| Services
  Cmd4 -->|persists| Services
  Cmd5 -->|persists| Services

  Services -->|depends on| Repos
  Q1 -->|depends on| Repos
  Q2 -->|calls| Repos

  Repos -->|implements| GORM
  GORM -->|maps| Mappers
  Mappers -->|converts| Models

  Models -->|CRUD| DB
  GORM -->|cache/session| Redis
  GORM -->|async jobs| Asynq
  Asynq -->|queue| Redis

  Services -.->| validates| VO
  Repos -.->| uses| Ent

Diagrama 2: Arquitectura Frontend (Astro Islands)

flowchart TB
    Astro[Astro] --> HTML[HTML]
    HTML --> Browser[Browser]

    Browser --> React[React Islands]
    Browser --> Svelte[Svelte Islands]
    Browser --> Static[Static .astro]

    React --> Store[Zustand]
    Svelte --> Store
    Store --> LS[localStorage]

    React --> Fetch[apiFetch]
    Fetch --> Fallback[Static Fallback]
    Fetch --> API[Go API]

    React --> Audio[HTML5 Audio]

Diagrama 3: Frontend + Backend en Funcionamiento

sequenceDiagram
    User->>Browser: Click Play
    Browser->>API: GET /playlists/:id
    API->>DB: Query playlist+songs
    DB->>API: PlaylistDTO
    API->>Browser: 200+JSON
    Browser->>Store: selectTrack
    Browser->>Audio: play

    User->>Browser: Search query
    Browser->>API: GET /songs/search
    API->>DB: ILIKE query
    DB->>API: Songs
    API->>Browser: 200

    User->>Browser: Login submit
    Browser->>API: POST /auth/login
    API->>API: Validate+JWT
    API->>Redis: Session cache
    API->>Browser: 200+JWT
    Browser->>Store: setTokens

Diagrama 4: Backend + Frontend + Deploy VPS

flowchart LR
    Dev --> Git
    Git -->|webhook| Dok[Dokploy]

    Dok --> Nginx
    Dok --> GoAPI
    Dok --> DB
    Dok --> Cache
    Dok --> Worker

    User --> Traefik
    Traefik --> Nginx
Nginx --> GoAPI

Diagramas detallados en docs/architecture/overview.md.


⚙️ Stack tecnológico

Frontend

Capa Herramienta Versión Por qué
Framework Astro 6.x SSG rápido con islands architecture
UI interactiva React 19.x Player, SearchView, CardPlayButton
UI declarativa Svelte 5.x Greeting — componente sin estado global
Estado global Zustand 5.x Store tipado por feature, sin boilerplate
Estilos TailwindCSS 4.x CSS-first con @theme y @utility
Iconos Lucide 1.x Stroke-based, consistentes
TypeScript TypeScript 5.9 Tipado estricto en todo el frontend

Backend

Capa Herramienta Versión Por qué
API Go + Gin 1.22 Hexagonal architecture, CQRS
ORM GORM 2.x Migraciones y queries tipadas
Base de datos PostgreSQL 16 Persistencia principal
Cache / queues Redis + Asynq 7 Cache de sesiones, jobs async
Auth JWT Access token + refresh token

Tooling

Herramienta Uso
pnpm 10.x Package manager, rápido y eficiente
Vitest 4 + jsdom Tests unitarios y de componente
ESLint + Prettier Lint + formato consistente
Husky + lint-staged Quality gates pre-commit / pre-push
Docker + nginx Imagen final ~25 MB, multi-stage
docker-compose Orquesta frontend + API + DB + Redis + Worker
Dokploy + Traefik Self-hosted PaaS con TLS automático

📁 Estructura del proyecto


freakfy/
├── api/ # Backend Go (hexagonal architecture)
│ ├── cmd/server/main.go # Entry point + config validation
│ ├── cmd/server/router.go # Route registration + /ready endpoint
│ ├── api/
│ │ ├── handlers/ # Split by capability (auth, playlist, song, user)
│ │ ├── middleware/ # Auth, request-id, body-limit, logging
│ │ └── dto/ # Request/Response DTOs
│ ├── application/
│ │ ├── commands/ # CQRS Commands
│ │ ├── queries/ # CQRS Queries
│ │ └── usecase/ # ReadFacade for orchestration
│ ├── domain/ # Pure domain (entities, interfaces, services)
│ ├── infrastructure/
│ │ └── repository/ # Repos + models + mappers (decoupled from domain)
│ └── pkg/ # Shared packages (apierror, config, response)
├── docker/
│ └── nginx.conf # Proxy /api/\* → Go API + static files
├── docs/ # Documentación técnica completa
├── public/
│ ├── fonts/ # CircularStd self-hosted
│ └── music/ # MP3s por playlist id
├── scripts/
│ └── fetch-playlists.mjs # Prebuild: genera src/generated/playlist-ids.json
├── src/
│ ├── features/ # Screaming Architecture por dominio
│ │ ├── auth/ # authStore.ts (access token en-memoria)
│ │ ├── player/ # Player.tsx, controles, store, shuffleHistory
│ │ ├── playlists/ # PlaylistCard, playlistsApi, tipos, mappers
│ │ ├── search/ # SearchView.tsx con debounce + API
│ │ └── settings/ # SettingsPanel.tsx
│ ├── shared/
│ │ ├── lib/api/ # apiFetch (auth, retry, timeout)
│ │ ├── lib/query/ # useQuery, useMutation (cache SWR)
│ │ ├── lib/utils/ # colors.ts
│ │ ├── types/api.types.ts # DTOs espejados del backend Go
│ │ └── ui/ # AsideMenu, TopBar, MobileBottomNav, Greeting...
│ ├── icons/ # SVG como .astro (paths de Lucide)
│ ├── layouts/Layout.astro # Grid global + TopBar + Player + ClientRouter
│ ├── pages/
│ │ ├── index.astro
│ │ ├── search.astro
│ │ ├── settings.astro
│ │ └── playlist/[id].astro # getStaticPaths desde generated/playlist-ids.json
│ ├── generated/
│ │ └── playlist-ids.json # Auto-generado por fetch-playlists.mjs en prebuild
│ ├── libreria/ # Datos estáticos de fallback (legacy)
│ └── styles/global.css # @theme tokens + spotify-grid utility
├── astro.config.mjs
├── docker-compose.yml # freakfy-web + freakfy-api + db + redis + worker
├── Dockerfile
├── tsconfig.json
└── vitest.config.ts

🚀 Quick start

Requisitos

  • Node.js 22+
  • pnpm 10+
  • Go 1.22+ (opcional para frontend-only)
  • Docker + docker-compose (para full-stack local)

Solo frontend (con datos estáticos de fallback)

git clone https://github.com/alvaroofernaandez/freakfy.git
cd freakfy
pnpm install
pnpm dev

Abre http://localhost:4321. Si la API Go no está corriendo, la app usa los datos estáticos de src/libreria/data.ts.

Full-stack local (con Go API)

# Terminal 1: API Go
cd api
go run cmd/server/main.go

# Terminal 2: Frontend
pnpm install
pnpm dev

El proxy de Vite reenvía /api/*http://localhost:8080 automáticamente.

Con Docker Compose (prod-like)

docker compose build
docker compose up

Abre http://localhost.

📖 Setup detallado: docs/development/setup.md.


📜 Scripts disponibles

Script Descripción
pnpm dev Servidor de desarrollo con HMR + proxy Vite → Go API
pnpm build Prebuild (fetch-playlists.mjs) + astro check + build
pnpm preview Preview del dist/ generado
pnpm lint ESLint sobre todo el repo
pnpm lint:fix ESLint con --fix
pnpm format Prettier write mode
pnpm format:check Prettier check mode (para CI)
pnpm type-check astro check (TS + Astro + Svelte)
pnpm test Vitest one-shot
pnpm test:watch Vitest en modo watch
pnpm test:coverage Reporte de cobertura

El script prebuild corre node scripts/fetch-playlists.mjs automáticamente antes de cada pnpm build. Si la API Go no está disponible, usa el playlist-ids.json existente como fallback.


🧪 Testing

pnpm test           # Una pasada
pnpm test:watch     # Watch mode
pnpm test:coverage  # Con cobertura
  • Runner: Vitest 4 con environment jsdom.
  • Componentes Svelte: @testing-library/svelte.
  • Coverage: @vitest/coverage-v8.
  • Tests actuales: lógica de shuffle de playlists (shuffleHistory.test.ts), shape de datos estáticos.

📖 Estrategia completa: docs/testing/strategy.md.


🚀 Deployment

FreakFy corre en un VPS propio gestionado con Dokploy, con docker-compose orquestando 5 servicios: el frontend nginx, la API Go, PostgreSQL, Redis y el worker Asynq.

%%{init: {
  "theme": "base",
  "themeVariables": {
    "primaryColor": "#2496ed",
    "primaryTextColor": "#ffffff",
    "primaryBorderColor": "#1d4ed8",
    "lineColor": "#2496ed"
  }
}}%%
flowchart LR
  dev[👨‍💻 Desarrollador] -->|git push main| gh[GitHub]
  gh -->|webhook| dkp[Dokploy VPS]
  dkp -->|docker compose build| img[Imágenes]
  img -->|docker compose up| nginx[nginx:alpine\nfreakfy-web]
  img --> api[Go API\nfreakfy-api]
  api --> db[(PostgreSQL)]
  api --> cache[(Redis)]
  traefik[Traefik + TLS] --> nginx
  traefik --> user[👤 Usuario]
  nginx -->|proxy /api/*| api

Servicios en docker-compose

Servicio Imagen Rol
freakfy-web nginx:alpine Sirve dist/ + proxy /api/*
freakfy-api Go + Gin API REST hexagonal
freakfy-db postgres:16 Base de datos principal
freakfy-redis redis:7 Cache + colas Asynq
freakfy-worker Go + Asynq Procesador de jobs en background

Dockerfile frontend — multi-stage

Stage Base Rol
base node:22-alpine Habilita corepack (pnpm), fija WORKDIR /app
deps base pnpm install --frozen-lockfile con cache
build base pnpm builddist/
runner nginx:1.27-alpine Copia dist/ + docker/nginx.conf

Ejecución local del contenedor

# Full stack
docker compose build
docker compose up

# Solo frontend
docker build -t freakfy:local .
docker run --rm -p 8080:80 freakfy:local

📖 Guía completa: docs/deployment/docker.md.


📏 Convenciones

  • Commits: Conventional Commits (feat:, fix:, chore:, docs:, ...).
  • Branches: feature/*, fix/*, chore/*, mergeadas a main vía PR.
  • Lint + format: husky pre-commit corre lint-staged.
  • Quality gates: husky pre-push corre lint + type-check + test.
  • Arquitectura: Screaming Architecture — nuevo código va en src/features/{nombre}/ o src/shared/.
  • Idioma en docs: español neutro.
  • Idioma en código: inglés (variables, funciones, comentarios técnicos).

📖 Workflow completo: docs/development/workflow.md.


📖 Documentación extendida

Sección Qué contiene
🏛️ Architecture Screaming Architecture, islands, API client, diagramas
🎵 Product Visión, features implementadas y pendientes
📊 Data Tipos TS, DTOs Go, contratos de API, mappers
🛠️ Development Setup, scripts, Git workflow
🚀 Deployment Docker, Dokploy, CI/CD, environments
🔒 Security CSP, headers, consideraciones
🧪 Testing Estrategia con Vitest + jsdom
📖 Guides Onboarding, troubleshooting

Índice navegable: docs/README.md.


🙏 Créditos

  • Autor: Álvaro Fernández.
  • Inspiración inicial: @midudev — tutorial de clon de Spotify con Astro.
  • Música: soundtracks de anime usados con fines estrictamente personales.

📝 Notas del autor

Este proyecto es personal. Me hacía ilusión llevarlo a cabo tanto como aprendizaje (Astro, islands, Screaming Architecture, backend Go, Zustand, Tailwind 4) como para tener un espacio donde guardar ordenadamente mi música favorita de anime. No pretende competir con Spotify — pretende ser mi Spotify.

— Álvaro

Top categories

Loading Svelte Themes