5 min read Last updated Jun 2026 Reiwa Sakura Tech team

DOCUMENTATION

Quickstart Guide

Get the full Reiwa Sakura Tech platform running in under 5 minutes. Everything you need — from local dev to production.

Astro 4 PHP 8.2 PostgreSQL 14 Docker 24 nginx

Prerequisites

ℹ️
You'll need these installed before you begin:
  • Node.js 20+ and npm 10+
  • PHP 8.2+ with pdo_pgsql, openssl, mbstring extensions
  • PostgreSQL 14+ (local or Docker)
  • Composer 2+
  • Docker 24+ (optional — only needed for the containerized workflow)

1. Clone & Install

1

Clone the monorepo and install all portal and API dependencies.

bash
git clone https://gitlab.reiwasakura.tech/Stephan_Filip/reiwasakura.tech
cd reiwasakura.tech

# Install Node dependencies for all 6 portals
npm install

# Install PHP dependencies for the API
cd api && composer install && cd ..

npm workspaces are not used here — each portal has its own package.json. The root npm install runs install across all workspaces via the root package.json scripts.

2. Database Setup

2

Create the PostgreSQL database, apply the schema, and load seed data.

bash
# Create the database
createdb reiwasakura_dev

# Apply the full schema (all 12 tables + indexes + triggers)
psql reiwasakura_dev < database/schema.sql

# Load dev seed data (1 super_admin, sample projects, jobs, blog posts)
psql reiwasakura_dev < database/seed.sql
📝
The seed file creates a super_admin account: admin@reiwasakura.tech / Admin@1234567 Change this immediately on any non-local environment.

3. Environment Variables

3

Copy the example env file and fill in your values. The API reads from api/.env; each portal reads its own .env (or .env.local).

bash
cp api/.env.example api/.env
nano api/.env
api/.env
# Database
DB_HOST=127.0.0.1
DB_PORT=5432
DB_NAME=reiwasakura_dev
DB_USER=postgres
DB_PASSWORD=your-db-password

# JWT — must be at least 32 random characters
JWT_SECRET=change-me-to-a-long-random-string-32chars+

# Mail (Poste.io or any SMTP)
MAIL_HOST=mail.reiwasakura.tech
MAIL_PORT=587
MAIL_USER=noreply@reiwasakura.tech
MAIL_PASS=your-smtp-password
MAIL_FROM=Reiwa Sakura Tech

# CORS — comma-separated list of allowed origins
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001,http://localhost:3002,http://localhost:3003,http://localhost:3004,http://localhost:3005

# Portal base URLs (used for cross-portal redirects)
PUBLIC_URL_FRONTEND=http://localhost:3000
PUBLIC_URL_AUTH=http://localhost:3004
PUBLIC_URL_ADMIN=http://localhost:3001
PUBLIC_URL_DOCS=http://localhost:3005

# API URL — keep as /api for relative proxy path
PUBLIC_API_URL=/api

4. Start Dev

4

Use the CLI agent to start all 7 services with a single command, or start them individually.

bash
# Start everything at once (recommended)
node cli-agent.js dev

# Or start a single portal
cd frontend && npm run dev     # :3000
cd admin    && npm run dev     # :3001
cd career   && npm run dev     # :3002
cd developer && npm run dev    # :3003
cd auth     && npm run dev     # :3004
cd docs     && npm run dev     # :3005

# Start the PHP API (runs via Docker or php -S)
cd api && php -S 0.0.0.0:3006 index.php

Alternatively, use docker compose up -d from the repo root to start everything containerised. The PHP API listens on 0.0.0.0:3006 inside the container, bound to 127.0.0.1:3006 on the host.

💡
All 7 services running. Open your browser and check:
Service URL Purpose
Frontend :3000 Public website
Admin :3001 CMS — users, projects, jobs, blog
Career :3002 Job listings & applications
Developer :3003 Kanban board, time tracking
Auth :3004 Login, logout, OTP flows
Docs :3005 This documentation site
API :3006 PHP REST API (internal)

What's next?

JWT Authentication

Every protected endpoint requires a Bearer JWT. The token is issued by POST /api/auth/login and stored in localStorage under the key rst_token as a JSON object — not as a raw string.

Session format
typescript
// What's stored in localStorage['rst_token']
interface Session {
  token:    string;   // Signed JWT (HS256, 1 h TTL)
  refresh?: string;   // Refresh token (7 day TTL) for silent rotation
  exp:      number;   // Epoch ms — when the access token expires
}

// Read the token safely
const raw  = localStorage.getItem('rst_token');
const sess: Session = JSON.parse(raw);
const jwt  = sess.token;
JWT payload
json
{
  "user_id":    42,
  "email":      "alice@reiwasakura.tech",
  "first_name": "Alice",
  "last_name":  "Chen",
  "role":       "developer",
  "iat":        1750000000,
  "exp":        1750003600
}
Making authenticated requests
typescript
// Login — store the returned session
const res  = await fetch('/api/auth/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email: 'alice@reiwasakura.tech', password: '...' }),
});
const { data } = await res.json();
// data.access_token, data.refresh_token, data.expires_in

localStorage.setItem('rst_token', JSON.stringify({
  token:   data.access_token,
  refresh: data.refresh_token,
  exp:     Date.now() + data.expires_in * 1000,
}));

// Authenticated request
const { data: tasks } = await fetch('/api/tasks', {
  headers: { Authorization: `Bearer ${JSON.parse(localStorage.getItem('rst_token')).token}` },
}).then(r => r.json());
⚠️
Domain restriction: Only @reiwasakura.tech email addresses can sign in. Passwords must be at least 12 characters. The presence cookie rst_logged_in=1 is set on .reiwasakura.tech as a UI hint only — the JWT is never stored in a cookie.
Silent token refresh

Each portal schedules a silent refresh 4 minutes before the access token expires. The refresh token is rotated on every use (refresh token rotation).

typescript
// POST /api/auth/refresh
const r = await fetch('/api/auth/refresh', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ refresh_token: sess.refresh }),
});
const { data } = await r.json();
// data.access_token  — new JWT
// data.refresh_token — new refresh token (old one is now invalid)
// data.expires_in    — seconds until new JWT expires

Theme System

All portals share a CSS custom property theme system defined in public/styles/theme.css. Toggle dark/light by calling window.__toggleTheme() — it writes to localStorage and updates the data-theme attribute on <html>.

css
/* Light mode — applied when data-theme="light" */
:root {
  --bg:         #faf8ff;
  --text:       #0d0b14;
  --accent:     #7c3aed;
  --surface:    #ffffff;
  --surface-2:  #f3f0ff;
  --border:     rgba(100,80,180,.12);
}

/* Dark mode — applied when data-theme="dark" */
[data-theme="dark"] {
  --bg:         #0d0b14;
  --text:       #e8e0ff;
  --accent:     #a96ef5;
  --surface:    #130f1e;
  --surface-2:  #1a1528;
  --border:     rgba(255,255,255,.08);
}

/* Accent colours available everywhere */
--purple: #a96ef5;
--pink:   #e8788a;
--teal:   #4ecdc4;
--gold:   #f7c948;
--green:  #3ddc84;
--red:    #ff6363;
💡
The theme is applied before first paint via an inline script in Layout.astro — no flash of unstyled content. The ThemeToggle component uses window.__toggleTheme() internally and flips [data-theme-icon] display attributes.