Skip to content

Commit ba4eee2

Browse files
authored
chore(core): architecture overview (#342)
* chore(core): architecture overview * chore(core): update doc * chore(core): format
1 parent 9f9b5db commit ba4eee2

3 files changed

Lines changed: 284 additions & 0 deletions

File tree

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
# Aurora Dashboard – Architecture Overview
2+
3+
![Simple Arhcitecture Overview](overview.png)
4+
5+
## Introduction
6+
7+
Aurora Dashboard is an alternative OpenStack user dashboard, inspired by the [SAP Cloud Infrastructure Elektra dashboard](https://github.com/SAP-cloud-infrastructure/elektra). Its focus is on allowing fine-grained control and self-service for end users as well as integrating interfaces for related non-OpenStack services. It is designed to be usable with a vanilla OpenStack installation but offers extensibility and configurability for specific custom usecases.
8+
9+
Aurora Dashboard is designed to be fast, modular, and developer-friendly, offering a clean and type-safe experience across the stack. By following a Backend-for-Frontend (BFF) architecture, we abstract away OpenStack complexity and deliver a UI that is decoupled, composable, and scalable.
10+
11+
This document describes the architecture, conventions, technologies, and implementation strategies used to build and maintain the Aurora Dashboard.
12+
13+
---
14+
15+
## Tech Stack and Tooling
16+
17+
| Area | Tooling |
18+
| ----------------- | ----------------------------------- |
19+
| UI Framework | React 19, juno |
20+
| Styling | Tailwind CSS v4 |
21+
| Routing | TanStack Router (v1) |
22+
| API Communication | tRPC + Zod |
23+
| Server Runtime | Fastify |
24+
| i18n | Lingui |
25+
| Dev Tools | Vite, Vitest, pnpm, TurboRepo |
26+
| Code Quality | ESLint, Husky, Conventional Commits |
27+
28+
---
29+
30+
## Monorepo Structure
31+
32+
Aurora uses a pnpm-based monorepo managed by TurboRepo. Shared tooling is centralized, and apps and packages are clearly separated.
33+
34+
```
35+
/aurora
36+
/apps
37+
/aurora-portal # Main frontend application
38+
/packages
39+
/aurora-sdk # tRPC client wrapper
40+
/signal-openstack # OpenStack integration layer
41+
/config # Shared tsconfig, eslint, tailwind config
42+
/template # Internal package scaffold template
43+
```
44+
45+
---
46+
47+
## Frontend Routing and State – TanStack Router
48+
49+
We use TanStack Router v1 with file-based routing to build our UI declaratively and with type safety at every level.
50+
51+
### File-based Routing Conventions
52+
53+
To maximize DX and enable type-safe navigation:
54+
55+
- All route files for dynamic segments use a $ prefix, e.g. /$projectId.tsx
56+
- UI subfolders that shouldn’t become routes (e.g., components) are prefixed with -, e.g. `-components/`
57+
- Each route export `component`, `loader`, and optionally `beforeLoad`
58+
- Internal folders like `-components` are excluded from route generation
59+
60+
Example route tree:
61+
62+
```
63+
routes/
64+
_auth/
65+
accounts/
66+
$accountId/
67+
projects/
68+
$projectId/
69+
compute/$
70+
network.tsx
71+
-components/
72+
ProjectSubNavigation.tsx
73+
```
74+
75+
### Shared Layout and Auth Guarding
76+
77+
The `_auth` route is a shared layout for all authenticated views. It uses `beforeLoad` to check or hydrate session state:
78+
79+
```ts
80+
export const Route = createFileRoute("/_auth")({
81+
component: RouteComponent,
82+
beforeLoad: async ({ context, location }) => {
83+
if (!context.auth?.isAuthenticated) {
84+
const token = await context.trpcClient?.auth.getCurrentUserSession.query()
85+
if (!token) {
86+
throw redirect({ to: "/auth/login", search: { redirect: location.href } })
87+
}
88+
context.auth?.login(token.user, token.expires_at)
89+
}
90+
},
91+
})
92+
```
93+
94+
This makes sure no child route loads without a scoped session.
95+
96+
### Rescoping Tokens in Loaders
97+
98+
OpenStack APIs require project- or domain-scoped tokens. These are set via tRPC mutations in route loaders:
99+
100+
```ts
101+
const data = await context.trpcClient?.auth.setCurrentScope.mutate({
102+
type: "project",
103+
projectId: params.projectId || "",
104+
})
105+
```
106+
107+
Components do not perform data fetching. They receive fully scoped data via loader return values.
108+
109+
### Declarative Subnavigation
110+
111+
Subnavigation is constructed declaratively using `linkOptions` and `useParams`, avoiding manual URL construction:
112+
113+
```tsx
114+
const options = [
115+
linkOptions({
116+
to: "/accounts/$accountId/projects/$projectId/compute/$",
117+
label: "Compute",
118+
params: { accountId: "accountId", projectId: "projectId" },
119+
}),
120+
]
121+
122+
export function ProjectSubNavigation() {
123+
const params = useParams({ from: "/_auth/accounts/$accountId/projects/$projectId" })
124+
return <SubNavigationLayout options={options} params={params} />
125+
}
126+
```
127+
128+
Benefits:
129+
130+
- No manual path building
131+
- Automatic active states
132+
- Purely param-driven context
133+
134+
Subnavigation is active-aware and context-dependent without additional logic.
135+
136+
---
137+
138+
## Server-Side Architecture – BFF with Fastify and tRPC
139+
140+
Aurora uses a Backend-for-Frontend (BFF) pattern implemented with Fastify. All API interactions go through tRPC routes, which internally communicate with OpenStack services using a unified abstraction layer (`signal-openstack`).
141+
142+
### Benefits of BFF
143+
144+
- UI remains clean and stateless
145+
- All API contracts are validated and typed
146+
- OpenStack logic is fully encapsulated
147+
- Enables token scoping, retry logic, and API stitching
148+
149+
### Folder Structure
150+
151+
Each domain is in a PascalCase folder (e.g., Compute, Network) and contains:
152+
153+
```
154+
Compute/
155+
routers/
156+
keypair.ts
157+
types/
158+
keypair.ts
159+
tests/
160+
keypair.test.ts
161+
```
162+
163+
### Example: Keypair Domain
164+
165+
#### Zod Schema
166+
167+
```ts
168+
export const keypairSchema = z.object({
169+
name: z.string(),
170+
public_key: z.string().optional(),
171+
fingerprint: z.string().optional(),
172+
type: z.union([z.literal("ssh"), z.literal("x509"), z.string()]).optional(),
173+
user_id: z.string().optional().nullable(),
174+
created_at: z.string().optional(),
175+
deleted: z.boolean().optional(),
176+
deleted_at: z.string().optional().nullable(),
177+
updated_at: z.string().optional().nullable(),
178+
id: z.number().optional(),
179+
private_key: z.string().optional().nullable(),
180+
})
181+
```
182+
183+
#### tRPC Router
184+
185+
```ts
186+
getKeypairsByProjectId: protectedProcedure.input(z.object({ projectId: z.string() })).query(async ({ input, ctx }) => {
187+
const session = await ctx.rescopeSession({ projectId: input.projectId })
188+
const compute = session?.service("compute")
189+
const response = await compute?.get("os-keypairs").then((r) => r.json())
190+
const parsed = keypairsResponseSchema.safeParse(response)
191+
return parsed.success ? parsed.data.keypairs.map((k) => k.keypair) : undefined
192+
})
193+
```
194+
195+
#### Vitest Tests
196+
197+
```ts
198+
describe("OpenStack Keypair Schema", () => {
199+
it("validates correct keypair", () => {
200+
const result = keypairSchema.safeParse({ name: "my-key" })
201+
expect(result.success).toBe(true)
202+
})
203+
204+
it("fails on missing name", () => {
205+
const result = keypairSchema.safeParse({})
206+
expect(result.success).toBe(false)
207+
})
208+
})
209+
```
210+
211+
---
212+
213+
## Testing Strategy
214+
215+
### Lingui + Vitest Setup
216+
217+
All component tests use `I18nProvider` english locale is default if not language is provided and activate language context via `act()`.
218+
219+
```tsx
220+
import { render, act } from "@testing-library/react"
221+
import { I18nProvider } from "@lingui/react"
222+
import { i18n } from "@lingui/core"
223+
import App from "./App"
224+
225+
const TestingProvider = ({ children }) => <I18nProvider i18n={i18n}>{children}</I18nProvider>
226+
227+
test("should translate to German", async () => {
228+
await act(() => i18n.activate("de"))
229+
const { findByText } = render(<App />, { wrapper: TestingProvider })
230+
expect(await findByText("Willkommen beim Aurora-Dashboard")).toBeInTheDocument()
231+
})
232+
```
233+
234+
---
235+
236+
## Infrastructure Context: Gardener
237+
238+
Aurora primarily targets OpenStack APIs, but we also integrate with **Gardener**, a Kubernetes-native service management platform. Gardener provides a standardized Kubernetes API abstraction for managing clusters and infrastructure services, which fits well into our model for domain-scoped control and tRPC-driven orchestration.
239+
240+
---
241+
242+
## Release Process – Semantic Release
243+
244+
We use **Semantic Release** to automate our versioning, changelog creation, and release tagging based on commit history.
245+
246+
### How It Works
247+
248+
Our release process has two main steps:
249+
250+
1. **Release Stage**
251+
Triggered by the `release` command. It runs Semantic Release, which:
252+
253+
- Analyzes commit messages using the **Conventional Commits** format
254+
- Decides the next version (major, minor, or patch)
255+
- Generates a changelog automatically
256+
- Tags the release in Git
257+
258+
2. **Promotion Stage**
259+
Triggered manually via a `promote` command. It:
260+
- Selects the Git tag created during the release
261+
- Deploys it to production
262+
263+
### Why We Use It
264+
265+
- No manual versioning – everything is based on commits
266+
- Automatic changelog generation
267+
- Clear and traceable releases
268+
- Safer, controlled production deployment
269+
270+
This setup reduces manual work and keeps our releases consistent and predictable.
271+
![Release diagram](release.png)
272+
273+
## Summary
274+
275+
Aurora Dashboard provides a robust, modern frontend and backend architecture with full type safety, modular structure, and OpenStack abstraction. Its key strengths include:
276+
277+
- File-based, loader-driven frontend with TanStack Router
278+
- Stateless and clean UI powered by scoped route context
279+
- Fully decoupled BFF server using Fastify, tRPC, and Zod
280+
- Declarative subnavigation and scoped auth token logic
281+
- Testable domain modules and schema validation
282+
- Support for hybrid backends including OpenStack and Gardener
283+
284+
Aurora is built to scale, to onboard developers quickly, and to support future cloud platforms with minimal coupling.

docs/overview.png

1.99 MB
Loading

docs/release.png

2.65 MB
Loading

0 commit comments

Comments
 (0)