| | |
|---|---|
| Version | 1.9.0 |
| Status | Draft |
| Author | Anthonius Munthi |
| Last Updated | March 2025 |
| Classification | Confidential |
This document defines the product requirements for a personal blog platform built with a modern, AI-assisted development workflow. The platform consists of three core applications: a public-facing Astro frontend, an administrative Next.js dashboard, and a Go REST API backend — all following industry-standard architectural patterns.
The system enables a content author to publish, manage, and deliver blog posts to readers, with a clean separation between public consumption (Astro) and administrative control (Next.js). Authentication is managed entirely by Better Auth, with the Astro frontend using API Key authentication for secure, read-only access to the Go REST API.
| Field | Value |
|---|---|
| Product Name | Omed |
| Target Users | Content creators, developers, technical writers |
| Platforms | Web (Desktop & Mobile responsive) |
| Launch Target | Q3 2025 |
| Tech Stack | Astro, Next.js (FSD), Go + EntGO, Better Auth, PostgreSQL, Turborepo, shadcn/ui (Mira), antigravity + stitch |
-
Provide a fast, SEO-optimized public blog with minimal operational overhead
-
Enable the author to manage all content through a secure admin dashboard
-
Build a scalable foundation that can evolve into a multi-author platform
-
Minimize costs by using open-source tooling and self-hostable infrastructure
-
Enforce clear separation of concerns via Feature-Sliced Design (FSD) on all frontends
-
Maintain a clean architecture in the Go API with no cross-layer dependencies
-
Achieve sub-500ms Time to First Byte (TTFB) for the public Astro frontend
-
Keep API key authentication stateless and verifiable via Better Auth
-
Ensure zero-downtime deployments with environment-based configuration
The platform follows a three-tier architecture. All data flows originate from user interactions on either the Astro frontend or the Next.js dashboard. Both communicate exclusively with the Go REST API. The dashboard hosts the Better Auth server, which issues API keys for the Astro frontend and JWT tokens for admin dashboard sessions.
┌──────────────────────────┐ ┌──────────────────────────┐
│ Astro Frontend │ │ Next.js Dashboard │
│ (Public Blog) │ │ (Admin) │
│ Auth: x-api-key │ │ Auth: Better Auth │
│ FSD Architecture │ │ Session + apiKey plugin│
└────────────┬─────────────┘ └────────────┬─────────────┘
│ GET /api/* │ POST/PUT/DELETE /api/admin/*
│ x-api-key header │ Authorization: Bearer <JWT>
└──────────────────┬──────────────┘
▼
┌───────────────────────────────┐
│ Go REST API │
│ Clean Architecture + EntGO │
│ APIKeyMiddleware (public) │
│ JWTMiddleware (admin) │
└───────────────┬───────────────┘
│
▼
┌───────────────────────────────┐
│ PostgreSQL │
│ (via EntGO ORM) │
└───────────────────────────────┘
| Layer | Application | Framework | Auth Method |
|---|---|---|---|
| Public Frontend | Astro Frontend | Astro + FSD | API Key (x-api-key) |
| Admin Dashboard | Next.js Dashboard | Next.js + FSD | Session (Better Auth) |
| API Backend | Go REST API | Go + Clean Arch + EntGO | Verify via Better Auth |
| Database | PostgreSQL | EntGO ORM | N/A |
The authentication mechanism is split across services with Better Auth as the single source of truth:
┌──────────────────────────────────────────────┐
│ Next.js Dashboard │
│ Better Auth Server (email/password login) │
│ → Admin manages posts, generates API keys │
└──────────────┬───────────────────────────────┘
│ issues API Key (blog_pub_xxx)
▼
┌──────────────────────────────────────────────┐
│ Astro Frontend │
│ Better Auth Client (apiKeyClient plugin) │
│ → Sends x-api-key on every request │
└──────────────┬───────────────────────────────┘
│ x-api-key header
▼
┌──────────────────────────────────────────────┐
│ Go REST API │
│ APIKeyMiddleware │
│ → Calls Better Auth /api/auth/api-key/verify
│ → Checks permissions (posts:read) │
└──────────────────────────────────────────────┘
-
Next.js Dashboard — Admin logs in via email/password. Better Auth issues a session cookie.
-
API Key Issuance — Authenticated admin generates a read-only API key (
prefix: blog_pub_) with scoped permissions (posts:read,tags:read) via/api/generate-api-key. -
Astro Frontend — Stores the API key as
PUBLIC_BLOG_API_KEYin environment variables. All requests to the Go API include thex-api-keyheader. -
Go REST API — The
APIKeyMiddlewareintercepts public requests and verifies the key by calling Better Auth's/api/auth/api-key/verifyendpoint. Invalid or expired keys receive a401response.
The Astro frontend is the primary public interface. It is a statically generated site served via CDN, optimized for SEO and performance. It consumes the Go REST API exclusively using an API key.
| Field | Value |
|---|---|
| Framework | Astro with Feature-Sliced Design (FSD) |
| Rendering | Static Site Generation (SSG) via getStaticPaths() |
| Auth | API Key — x-api-key header on all Go API requests |
| Hosting | GitHub Pages (kilip.github.io) |
| Base URL | https://kilip.github.io |
| Port (dev) | 4321 |
FSD Layer Responsibilities:
| Layer | Path | Responsibility |
|---|---|---|
| shared | shared/api/client.ts | Instantiates createApiClient from @omed/api — injects x-api-key and base URL |
| shared | shared/api/auth-client.ts | Better Auth client with apiKeyClient plugin |
| entities | entities/post/ | Post model, PostCard UI component, postApi fetch functions (via @omed/api) |
| entities | entities/tag/ | Tag model, TagBadge UI component, tagApi fetch functions (via @omed/api) |
| features | features/post-search/ | Client-side search interaction and state |
| features | features/post-filter/ | Tag-based filtering logic |
| widgets | widgets/post-list/ | Assembled post grid combining entities and features |
| widgets | widgets/header/, widgets/footer/ | Site-wide layout components |
| pages | pages/ | Astro route pages: index.astro, posts/[slug].astro, tags/[slug].astro |
Folder Structure:
ui/apps/blog/
├── package.json # name: "@omed/blog"
├── public/
│ ├── favicon.svg
│ ├── robots.txt
│ └── sitemap-index.xml # generated by @astrojs/sitemap
├── src/
│ ├── app/
│ │ └── styles/
│ │ └── global.css
│ ├── pages/
│ │ ├── index.astro # Homepage — paginated post list
│ │ ├── posts/
│ │ │ └── [slug].astro # Post detail page
│ │ └── tags/
│ │ └── [slug].astro # Tag filtered post list
│ ├── widgets/
│ │ ├── header/
│ │ │ ├── ui/Header.astro
│ │ │ └── index.ts
│ │ ├── footer/
│ │ │ ├── ui/Footer.astro
│ │ │ └── index.ts
│ │ └── post-list/
│ │ ├── ui/PostList.astro
│ │ └── index.ts
│ ├── features/
│ │ ├── post-search/
│ │ │ ├── ui/SearchBar.astro
│ │ │ └── index.ts
│ │ └── post-filter/
│ │ ├── ui/TagFilter.astro
│ │ └── index.ts
│ ├── entities/
│ │ ├── post/
│ │ │ ├── api/postApi.ts
│ │ │ ├── model/post.types.ts
│ │ │ ├── ui/PostCard.astro
│ │ │ └── index.ts
│ │ └── tag/
│ │ ├── api/tagApi.ts
│ │ ├── model/tag.types.ts
│ │ ├── ui/TagBadge.astro
│ │ └── index.ts
│ └── shared/
│ ├── api/
│ │ ├── client.ts # instantiates createApiClient from @omed/api, config from @omed/config/blog
│ │ └── auth-client.ts # Better Auth apiKeyClient
│ ├── config/
│ │ └── env.ts # typed env variable access
│ └── ui/
│ ├── Button.astro
│ └── Prose.astro
├── astro.config.mjs
└── tsconfig.json
The dashboard is a protected SPA for content management. Accessible only to authenticated administrators. It hosts the Better Auth server and API key management UI.
| Field | Value |
|---|---|
| Framework | Next.js 14 App Router with Feature-Sliced Design (FSD) |
| Rendering | Server-Side Rendering (SSR) with React Server Components |
| UI Library | shadcn/ui — Base: Base UI, Preset: Mira |
| Auth | Better Auth — email/password session with apiKey plugin |
| Hosting | Docker (local) via Cloudflare Tunnel |
| URL | https://dash.itstoni.com |
| Port (dev) | 3000 |
Key Dashboard Pages:
| Route | Description |
|---|---|
| (auth)/login | Login form via Better Auth email/password |
| (dashboard)/posts | Post management table with create, edit, delete |
| (dashboard)/tags | Tag management with CRUD operations |
| (dashboard)/settings | Profile settings and API key generation for Astro |
API Routes:
| Route | Description |
|---|---|
| app/api/auth/[...nextauth]/route.ts | Better Auth catch-all handler |
| app/api/generate-api-key/route.ts | Issues scoped API key for Astro frontend |
Folder Structure:
ui/apps/dash/
├── package.json # name: "@omed/dash"
├── public/
├── src/
│ ├── app/
│ │ ├── layout.tsx
│ │ ├── (auth)/
│ │ │ └── login/
│ │ │ └── page.tsx
│ │ ├── (dashboard)/
│ │ │ ├── layout.tsx # protected layout — redirects if unauthenticated
│ │ │ ├── posts/
│ │ │ │ ├── page.tsx # post list table
│ │ │ │ ├── new/page.tsx # create post
│ │ │ │ └── [id]/
│ │ │ │ └── edit/page.tsx
│ │ │ ├── tags/
│ │ │ │ └── page.tsx
│ │ │ └── settings/
│ │ │ └── page.tsx # API key management
│ │ └── api/
│ │ ├── auth/
│ │ │ └── [...nextauth]/
│ │ │ └── route.ts
│ │ └── generate-api-key/
│ │ └── route.ts
│ ├── widgets/
│ │ ├── sidebar/
│ │ ├── post-table/
│ │ └── post-editor/
│ │ └── ui/PostEditor.tsx # Markdown editor with live preview
│ ├── features/
│ │ ├── auth-form/
│ │ ├── post-publish/
│ │ └── tag-manage/
│ ├── entities/
│ │ ├── post/
│ │ │ ├── api/postApi.ts
│ │ │ └── model/post.types.ts
│ │ └── tag/
│ │ ├── api/tagApi.ts
│ │ └── model/tag.types.ts
│ └── shared/
│ ├── api/
│ │ ├── client.ts # instantiates createAdminClient from @omed/api, config from @omed/config/dash
│ │ └── auth-client.ts # Better Auth client (session)
│ ├── config/
│ │ └── env.ts
│ └── ui/
│ ├── Button.tsx
│ ├── Input.tsx
│ └── Modal.tsx
├── next.config.ts
└── tsconfig.json
The Go REST API is the single data authority. It exposes public read endpoints (protected by API key) and admin write endpoints (protected by JWT). Built following Clean Architecture principles with EntGO as the ORM.
| Field | Value |
|---|---|
| Language | Go 1.22+ |
| Architecture | Clean Architecture (domain / service / repository / delivery) |
| ORM | EntGO (entgo.io) with PostgreSQL |
| Router | GoFiber (gofiber/fiber) v2 |
| Auth | API Key middleware (public) + JWT middleware (admin) |
| Hosting | Docker (local) via Cloudflare Tunnel |
| URL | https://api.itstoni.com |
| Port (dev) | 8080 |
Clean Architecture Layers:
| Layer | Path | Responsibility |
|---|---|---|
| Domain | internal/domain/ | Core entities (Post, Tag, User) and repository interfaces. No external dependencies. |
| Service | internal/service/ | Business logic. Depends only on domain interfaces. |
| Repository | internal/repository/ | EntGO implementations of domain repository interfaces. |
| Delivery | internal/delivery/http/ | Fiber router, HTTP handlers, middleware (APIKeyMiddleware, JWTMiddleware). |
| Schema | internal/ent/schema/ | EntGO schema definitions for code generation. |
Folder Structure:
api/
├── cmd/
│ └── server/
│ └── main.go # entry point — wires dependencies, starts HTTP server
├── internal/
│ ├── domain/
│ │ ├── post.go # Post entity struct + repository interface
│ │ ├── tag.go # Tag entity struct + repository interface
│ │ └── errors.go # domain-level sentinel errors
│ ├── service/
│ │ ├── post_service.go # business logic: list, get, create, update, delete
│ │ ├── post_service_test.go
│ │ ├── tag_service.go
│ │ └── tag_service_test.go
│ ├── repository/
│ │ ├── post_repo.go # EntGO implementation of PostRepository
│ │ └── tag_repo.go
│ ├── delivery/
│ │ └── http/
│ │ ├── router.go # fiber app setup, middleware chain
│ │ ├── middleware/
│ │ │ ├── api_key.go # APIKeyMiddleware — verifies x-api-key via Better Auth
│ │ │ ├── jwt.go # JWTMiddleware — validates Bearer JWT for admin routes
│ │ │ └── cors.go
│ │ └── handler/
│ │ ├── post_handler.go
│ │ └── tag_handler.go
│ └── ent/
│ ├── schema/
│ │ ├── post.go # EntGO schema definition for Post
│ │ └── tag.go
│ └── ... # generated EntGO client code (do not edit manually)
├── migrations/ # versioned SQL migration files
│ ├── 001_create_posts.sql
│ └── 002_create_tags.sql
├── openapi.yaml # OpenAPI 3.1 spec — source of truth for @omed/api codegen
├── config/
│ └── config.go # env-based config loader
├── .env.example
├── go.mod
├── go.sum
└── Makefile # targets: build, test, generate, migrate, openapi
@omed/api is a shared TypeScript package consumed by both @omed/blog and @omed/dash. It is generated from api/openapi.yaml using openapi-typescript and provides fully-typed fetch clients for all Go REST API endpoints. Neither app hand-writes API fetch logic — all HTTP calls go through this package.
| Field | Value |
|---|---|
| Package name | @omed/api |
| Generator | openapi-typescript + openapi-fetch |
| Source spec | api/openapi.yaml (OpenAPI 3.1) |
| Output | ui/packages/api/src/schema.d.ts (generated, do not edit) |
| Consumers | @omed/blog, @omed/dash |
Folder Structure:
ui/packages/api/
├── package.json # name: "@omed/api"
├── tsconfig.json
├── scripts/
│ └── generate.sh # runs openapi-typescript against ../../api/openapi.yaml
└── src/
├── schema.d.ts # AUTO-GENERATED — do not edit manually
├── client.ts # createClient() wrapper with base URL + x-api-key injection
├── admin-client.ts # createAdminClient() wrapper with Bearer JWT injection
└── index.ts # re-exports: client, adminClient, and all path types
package.json
{
"name": "@omed/api",
"version": "0.0.0",
"private": true,
"scripts": {
"generate": "openapi-typescript ../../api/openapi.yaml -o src/schema.d.ts",
"build": "tsc",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"openapi-fetch": "latest"
},
"devDependencies": {
"openapi-typescript": "latest",
"typescript": "latest"
}
}
src/client.ts — public read-only client (used by @omed/blog):
import createClient from "openapi-fetch";
import type { paths } from "./schema";
export const createApiClient = (baseUrl: string, apiKey: string) =>
createClient<paths>({
baseUrl,
headers: { "x-api-key": apiKey },
});src/admin-client.ts — admin client with JWT (used by @omed/dash):
import createClient from "openapi-fetch";
import type { paths } from "./schema";
export const createAdminClient = (baseUrl: string, token: string) =>
createClient<paths>({
baseUrl,
headers: { Authorization: `Bearer ${token}` },
});Codegen Workflow:
api/openapi.yaml ──(openapi-typescript)──► ui/packages/api/src/schema.d.ts
│
┌─────────────┴─────────────┐
▼ ▼
@omed/blog @omed/dash
(createApiClient) (createAdminClient)
-
Developer updates
api/openapi.yamlwhen a Go handler changes. -
Run
pnpm --filter @omed/api generate(orpnpm generatefromui/) to regenerateschema.d.ts. -
TypeScript compiler immediately surfaces any breaking changes in
@omed/blogor@omed/dash. -
The
generatestep runs in CI (blog.yml,dash.yml) before the build step to ensure the schema is always fresh.
Usage in @omed/blog:
// ui/apps/blog/src/shared/api/client.ts
import { createApiClient } from "@omed/api";
import c from "@omed/config/blog";
export const apiClient = createApiClient(c.api.baseUrl, c.api.key);
// entities/post/api/postApi.ts
import { apiClient } from "@/shared/api/client";
export const getPosts = async (page = 1) => {
const { data, error } = await apiClient.GET("/api/posts", {
params: { query: { page, per_page: 9 } },
});
if (error) throw error;
return data;
};Usage in @omed/dash:
// ui/apps/dash/src/shared/api/client.ts
import { createAdminClient } from "@omed/api";
import c from "@omed/config/dash";
export const getAdminClient = (token: string) =>
createAdminClient(c.api.baseUrl, token);
// features/post-publish/api/publishPost.ts
import { getAdminClient } from "@/shared/api/client";
export const createPost = async (token: string, body: PostCreateBody) => {
const client = getAdminClient(token);
const { data, error } = await client.POST("/api/admin/posts", { body });
if (error) throw error;
return data;
};