@getstrata/starter 0.1.4 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +9 -20
  2. package/dist/cli.js +2400 -47
  3. package/dist/templates/.env.example +4 -1
  4. package/dist/templates/overlays/api/docs/API.md +9 -0
  5. package/dist/templates/overlays/server-htmx/public/assets/app.css +89 -0
  6. package/dist/templates/overlays/server-htmx/resources/views/errors/error.eta +14 -0
  7. package/dist/templates/overlays/server-htmx/resources/views/errors/forbidden.eta +4 -0
  8. package/dist/templates/overlays/server-htmx/resources/views/errors/not-found.eta +4 -0
  9. package/dist/templates/overlays/server-htmx/resources/views/layouts/app.eta +34 -0
  10. package/dist/templates/overlays/server-htmx/resources/views/organizations/_table.eta +23 -0
  11. package/dist/templates/overlays/server-htmx/resources/views/organizations/index.eta +37 -0
  12. package/dist/templates/overlays/server-htmx/resources/views/pages/home.eta +4 -0
  13. package/dist/templates/overlays/server-htmx/resources/views/partials/_flash.eta +3 -0
  14. package/dist/templates/overlays/spa-react/frontend/build.ts +17 -0
  15. package/dist/templates/overlays/spa-react/frontend/bun-env.d.ts +4 -0
  16. package/dist/templates/overlays/spa-react/frontend/bun.lock +51 -0
  17. package/dist/templates/overlays/spa-react/frontend/dev-server.ts +66 -0
  18. package/dist/templates/overlays/spa-react/frontend/index.html +12 -0
  19. package/dist/templates/overlays/spa-react/frontend/package.json +21 -0
  20. package/dist/templates/overlays/spa-react/frontend/src/App.tsx +80 -0
  21. package/dist/templates/overlays/spa-react/frontend/src/api/client.ts +86 -0
  22. package/dist/templates/overlays/spa-react/frontend/src/app.css +98 -0
  23. package/dist/templates/overlays/spa-react/frontend/src/auth/AuthContext.tsx +103 -0
  24. package/dist/templates/overlays/spa-react/frontend/src/auth/tokenStorage.ts +15 -0
  25. package/dist/templates/overlays/spa-react/frontend/src/main.tsx +22 -0
  26. package/dist/templates/overlays/spa-react/frontend/src/pages/LoginPage.tsx +67 -0
  27. package/dist/templates/overlays/spa-react/frontend/src/pages/NotFoundPage.tsx +12 -0
  28. package/dist/templates/overlays/spa-react/frontend/src/pages/OrganizationsPage.tsx +69 -0
  29. package/dist/templates/overlays/spa-react/frontend/src/pages/ProjectsPage.tsx +65 -0
  30. package/dist/templates/overlays/spa-react/frontend/src/pages/TasksPage.tsx +65 -0
  31. package/dist/templates/{templates → overlays/spa-react/frontend}/tsconfig.json +9 -6
  32. package/dist/templates/package.json +2 -2
  33. package/package.json +3 -3
  34. package/dist/templates/templates/.env.example +0 -19
  35. package/dist/templates/templates/docker-compose.yml +0 -14
  36. package/dist/templates/templates/package.json +0 -22
  37. package/dist/templates/templates/public/assets/site.css +0 -29
  38. package/dist/templates/templates/src/bootstrap/config.ts +0 -18
  39. package/dist/templates/templates/src/bootstrap/createApp.ts +0 -93
  40. package/dist/templates/templates/src/bootstrap/database.ts +0 -30
  41. package/dist/templates/templates/src/bootstrap/preload.ts +0 -5
  42. package/dist/templates/templates/src/bootstrap/providers/auth.ts +0 -41
  43. package/dist/templates/templates/src/bootstrap/providers/cache.ts +0 -28
  44. package/dist/templates/templates/src/bootstrap/providers/config.ts +0 -27
  45. package/dist/templates/templates/src/bootstrap/providers/index.ts +0 -14
  46. package/dist/templates/templates/src/bootstrap/providers/storage.ts +0 -11
  47. package/dist/templates/templates/src/bootstrap/server.ts +0 -25
  48. package/dist/templates/templates/src/db/fresh.ts +0 -19
  49. package/dist/templates/templates/src/db/migrate.ts +0 -35
  50. package/dist/templates/templates/src/lib/view.ts +0 -21
  51. package/dist/templates/templates/src/modules/site/index.ts +0 -29
  52. package/dist/templates/templates/src/routes.ts +0 -6
  53. package/dist/templates/templates/strata.config.ts +0 -7
  54. package/dist/templates/templates/views/home.eta +0 -5
  55. package/dist/templates/templates/views/layouts/app.eta +0 -18
@@ -0,0 +1,98 @@
1
+ :root {
2
+ color: #0f172a;
3
+ background: #f8fafc;
4
+ font-family: system-ui, sans-serif;
5
+ line-height: 1.5;
6
+ }
7
+
8
+ * {
9
+ box-sizing: border-box;
10
+ }
11
+
12
+ body {
13
+ margin: 0;
14
+ }
15
+
16
+ a {
17
+ color: #2563eb;
18
+ }
19
+
20
+ .app-shell {
21
+ max-width: 960px;
22
+ margin: 0 auto;
23
+ padding: 1.5rem;
24
+ }
25
+
26
+ .site-header {
27
+ display: flex;
28
+ justify-content: space-between;
29
+ align-items: center;
30
+ gap: 1rem;
31
+ margin-bottom: 1.5rem;
32
+ padding-bottom: 1rem;
33
+ border-bottom: 1px solid #e2e8f0;
34
+ }
35
+
36
+ .site-header nav {
37
+ display: flex;
38
+ gap: 1rem;
39
+ flex-wrap: wrap;
40
+ }
41
+
42
+ .card {
43
+ background: #fff;
44
+ border: 1px solid #e2e8f0;
45
+ border-radius: 0.75rem;
46
+ padding: 1rem;
47
+ margin-bottom: 1rem;
48
+ }
49
+
50
+ .stack-form {
51
+ display: grid;
52
+ gap: 0.75rem;
53
+ max-width: 24rem;
54
+ }
55
+
56
+ .stack-form label {
57
+ display: grid;
58
+ gap: 0.25rem;
59
+ }
60
+
61
+ .stack-form input {
62
+ padding: 0.5rem 0.75rem;
63
+ border: 1px solid #cbd5e1;
64
+ border-radius: 0.5rem;
65
+ }
66
+
67
+ button {
68
+ cursor: pointer;
69
+ border: none;
70
+ border-radius: 0.5rem;
71
+ padding: 0.5rem 0.875rem;
72
+ background: #2563eb;
73
+ color: #fff;
74
+ }
75
+
76
+ button.secondary {
77
+ background: #64748b;
78
+ }
79
+
80
+ .error {
81
+ color: #b91c1c;
82
+ }
83
+
84
+ table {
85
+ width: 100%;
86
+ border-collapse: collapse;
87
+ }
88
+
89
+ th,
90
+ td {
91
+ text-align: left;
92
+ padding: 0.5rem 0.75rem;
93
+ border-bottom: 1px solid #e2e8f0;
94
+ }
95
+
96
+ .hint {
97
+ color: #64748b;
98
+ }
@@ -0,0 +1,103 @@
1
+ import {
2
+ createContext,
3
+ type ReactNode,
4
+ useCallback,
5
+ useContext,
6
+ useEffect,
7
+ useMemo,
8
+ useState,
9
+ } from "react";
10
+ import { apiFetch } from "../api/client";
11
+ import { clearToken, readToken, writeToken } from "./tokenStorage";
12
+
13
+ interface AuthUser {
14
+ id: number;
15
+ name: string;
16
+ email: string;
17
+ role: string;
18
+ }
19
+
20
+ interface AuthContextValue {
21
+ token: string | null;
22
+ user: AuthUser | null;
23
+ loading: boolean;
24
+ login: (email: string, password: string) => Promise<void>;
25
+ logout: () => void;
26
+ }
27
+
28
+ const AuthContext = createContext<AuthContextValue | null>(null);
29
+
30
+ function AuthProvider({ children }: { children: ReactNode }) {
31
+ const [token, setToken] = useState<string | null>(() => readToken());
32
+ const [user, setUser] = useState<AuthUser | null>(null);
33
+ const [loading, setLoading] = useState(Boolean(readToken()));
34
+
35
+ const loadProfile = useCallback(async (activeToken: string) => {
36
+ const profile = await apiFetch<AuthUser>("/auth/me", { token: activeToken });
37
+ setUser(profile);
38
+ }, []);
39
+
40
+ useEffect(() => {
41
+ if (!token) {
42
+ setUser(null);
43
+ setLoading(false);
44
+ return;
45
+ }
46
+
47
+ loadProfile(token)
48
+ .catch(() => {
49
+ clearToken();
50
+ setToken(null);
51
+ setUser(null);
52
+ })
53
+ .finally(() => {
54
+ setLoading(false);
55
+ });
56
+ }, [loadProfile, token]);
57
+
58
+ const login = useCallback(
59
+ async (email: string, password: string) => {
60
+ const body = await apiFetch<{ token: string }>("/auth/login", {
61
+ method: "POST",
62
+ body: JSON.stringify({ email, password }),
63
+ });
64
+
65
+ writeToken(body.token);
66
+ setToken(body.token);
67
+ await loadProfile(body.token);
68
+ },
69
+ [loadProfile],
70
+ );
71
+
72
+ const logout = useCallback(() => {
73
+ clearToken();
74
+ setToken(null);
75
+ setUser(null);
76
+ }, []);
77
+
78
+ const value = useMemo(
79
+ () => ({
80
+ token,
81
+ user,
82
+ loading,
83
+ login,
84
+ logout,
85
+ }),
86
+ [loading, login, logout, token, user],
87
+ );
88
+
89
+ return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
90
+ }
91
+
92
+ function useAuth(): AuthContextValue {
93
+ const context = useContext(AuthContext);
94
+
95
+ if (!context) {
96
+ throw new Error("useAuth must be used within AuthProvider");
97
+ }
98
+
99
+ return context;
100
+ }
101
+
102
+ export type { AuthUser };
103
+ export { AuthProvider, useAuth };
@@ -0,0 +1,15 @@
1
+ const TOKEN_KEY = "strata_api_token";
2
+
3
+ function readToken(): string | null {
4
+ return localStorage.getItem(TOKEN_KEY);
5
+ }
6
+
7
+ function writeToken(token: string): void {
8
+ localStorage.setItem(TOKEN_KEY, token);
9
+ }
10
+
11
+ function clearToken(): void {
12
+ localStorage.removeItem(TOKEN_KEY);
13
+ }
14
+
15
+ export { clearToken, readToken, TOKEN_KEY, writeToken };
@@ -0,0 +1,22 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { BrowserRouter } from "react-router-dom";
4
+ import App from "./App";
5
+ import { AuthProvider } from "./auth/AuthContext";
6
+ import "./app.css";
7
+
8
+ const rootElement = document.getElementById("root");
9
+
10
+ if (!rootElement) {
11
+ throw new Error("Root element #root not found");
12
+ }
13
+
14
+ createRoot(rootElement).render(
15
+ <StrictMode>
16
+ <BrowserRouter basename="/app">
17
+ <AuthProvider>
18
+ <App />
19
+ </AuthProvider>
20
+ </BrowserRouter>
21
+ </StrictMode>,
22
+ );
@@ -0,0 +1,67 @@
1
+ import { type FormEvent, useState } from "react";
2
+ import { Navigate, useNavigate } from "react-router-dom";
3
+ import { ApiError } from "../api/client";
4
+ import { useAuth } from "../auth/AuthContext";
5
+
6
+ export default function LoginPage() {
7
+ const { login, token } = useAuth();
8
+ const navigate = useNavigate();
9
+ const [email, setEmail] = useState("admin@example.test");
10
+ const [password, setPassword] = useState("password");
11
+ const [error, setError] = useState<string | null>(null);
12
+ const [submitting, setSubmitting] = useState(false);
13
+
14
+ if (token) {
15
+ return <Navigate to="/organizations" replace />;
16
+ }
17
+
18
+ async function onSubmit(event: FormEvent<HTMLFormElement>) {
19
+ event.preventDefault();
20
+ setSubmitting(true);
21
+ setError(null);
22
+
23
+ try {
24
+ await login(email, password);
25
+ navigate("/organizations");
26
+ } catch (cause) {
27
+ setError(cause instanceof ApiError ? cause.message : "Login failed");
28
+ } finally {
29
+ setSubmitting(false);
30
+ }
31
+ }
32
+
33
+ return (
34
+ <section className="card">
35
+ <h1>Sign in</h1>
36
+ <p className="hint">Use seeded credentials or your own API user.</p>
37
+
38
+ {error ? <p className="error">{error}</p> : null}
39
+
40
+ <form className="stack-form" onSubmit={onSubmit}>
41
+ <label>
42
+ Email
43
+ <input
44
+ type="email"
45
+ value={email}
46
+ onChange={(event) => setEmail(event.target.value)}
47
+ required
48
+ />
49
+ </label>
50
+
51
+ <label>
52
+ Password
53
+ <input
54
+ type="password"
55
+ value={password}
56
+ onChange={(event) => setPassword(event.target.value)}
57
+ required
58
+ />
59
+ </label>
60
+
61
+ <button type="submit" disabled={submitting}>
62
+ {submitting ? "Signing in…" : "Sign in"}
63
+ </button>
64
+ </form>
65
+ </section>
66
+ );
67
+ }
@@ -0,0 +1,12 @@
1
+ import { Link } from "react-router-dom";
2
+
3
+ export default function NotFoundPage() {
4
+ return (
5
+ <section className="card">
6
+ <h1>Not found</h1>
7
+ <p>
8
+ Return to <Link to="/organizations">organizations</Link>.
9
+ </p>
10
+ </section>
11
+ );
12
+ }
@@ -0,0 +1,69 @@
1
+ import { useEffect, useState } from "react";
2
+ import { Link } from "react-router-dom";
3
+ import { apiFetch } from "../api/client";
4
+ import { useAuth } from "../auth/AuthContext";
5
+
6
+ interface Organization {
7
+ id: number;
8
+ name: string;
9
+ slug: string;
10
+ }
11
+
12
+ interface PaginatedOrganizations {
13
+ data: Organization[];
14
+ meta: {
15
+ total: number;
16
+ };
17
+ }
18
+
19
+ export default function OrganizationsPage() {
20
+ const { token } = useAuth();
21
+ const [organizations, setOrganizations] = useState<Organization[]>([]);
22
+ const [error, setError] = useState<string | null>(null);
23
+
24
+ useEffect(() => {
25
+ if (!token) {
26
+ return;
27
+ }
28
+
29
+ apiFetch<PaginatedOrganizations>("/organizations", { token })
30
+ .then((body) => setOrganizations(body.data))
31
+ .catch((cause: unknown) => {
32
+ setError(cause instanceof Error ? cause.message : "Failed to load organizations");
33
+ });
34
+ }, [token]);
35
+
36
+ return (
37
+ <section>
38
+ <h1>Organizations</h1>
39
+ <p className="hint">{organizations.length} loaded from the JSON API.</p>
40
+
41
+ {error ? <p className="error">{error}</p> : null}
42
+
43
+ <div className="card">
44
+ <table>
45
+ <thead>
46
+ <tr>
47
+ <th>Name</th>
48
+ <th>Slug</th>
49
+ </tr>
50
+ </thead>
51
+ <tbody>
52
+ {organizations.map((organization) => (
53
+ <tr key={organization.id}>
54
+ <td>{organization.name}</td>
55
+ <td>
56
+ <code>{organization.slug}</code>
57
+ </td>
58
+ </tr>
59
+ ))}
60
+ </tbody>
61
+ </table>
62
+ </div>
63
+
64
+ <p>
65
+ Continue to <Link to="/projects">projects</Link> or <Link to="/tasks">tasks</Link>.
66
+ </p>
67
+ </section>
68
+ );
69
+ }
@@ -0,0 +1,65 @@
1
+ import { useEffect, useState } from "react";
2
+ import { Link } from "react-router-dom";
3
+ import { apiFetch } from "../api/client";
4
+ import { useAuth } from "../auth/AuthContext";
5
+
6
+ interface Project {
7
+ id: number;
8
+ name: string;
9
+ status: string;
10
+ organization_id: number;
11
+ }
12
+
13
+ interface PaginatedProjects {
14
+ data: Project[];
15
+ }
16
+
17
+ export default function ProjectsPage() {
18
+ const { token } = useAuth();
19
+ const [projects, setProjects] = useState<Project[]>([]);
20
+ const [error, setError] = useState<string | null>(null);
21
+
22
+ useEffect(() => {
23
+ if (!token) {
24
+ return;
25
+ }
26
+
27
+ apiFetch<PaginatedProjects>("/projects?include=organization", { token })
28
+ .then((body) => setProjects(body.data))
29
+ .catch((cause: unknown) => {
30
+ setError(cause instanceof Error ? cause.message : "Failed to load projects");
31
+ });
32
+ }, [token]);
33
+
34
+ return (
35
+ <section>
36
+ <h1>Projects</h1>
37
+ {error ? <p className="error">{error}</p> : null}
38
+
39
+ <div className="card">
40
+ <table>
41
+ <thead>
42
+ <tr>
43
+ <th>Name</th>
44
+ <th>Status</th>
45
+ <th>Organization</th>
46
+ </tr>
47
+ </thead>
48
+ <tbody>
49
+ {projects.map((project) => (
50
+ <tr key={project.id}>
51
+ <td>{project.name}</td>
52
+ <td>{project.status}</td>
53
+ <td>{project.organization_id}</td>
54
+ </tr>
55
+ ))}
56
+ </tbody>
57
+ </table>
58
+ </div>
59
+
60
+ <p>
61
+ Back to <Link to="/organizations">organizations</Link>.
62
+ </p>
63
+ </section>
64
+ );
65
+ }
@@ -0,0 +1,65 @@
1
+ import { useEffect, useState } from "react";
2
+ import { Link } from "react-router-dom";
3
+ import { apiFetch } from "../api/client";
4
+ import { useAuth } from "../auth/AuthContext";
5
+
6
+ interface Task {
7
+ id: number;
8
+ title: string;
9
+ status: string;
10
+ project_id: number;
11
+ }
12
+
13
+ interface PaginatedTasks {
14
+ data: Task[];
15
+ }
16
+
17
+ export default function TasksPage() {
18
+ const { token } = useAuth();
19
+ const [tasks, setTasks] = useState<Task[]>([]);
20
+ const [error, setError] = useState<string | null>(null);
21
+
22
+ useEffect(() => {
23
+ if (!token) {
24
+ return;
25
+ }
26
+
27
+ apiFetch<PaginatedTasks>("/tasks", { token })
28
+ .then((body) => setTasks(body.data))
29
+ .catch((cause: unknown) => {
30
+ setError(cause instanceof Error ? cause.message : "Failed to load tasks");
31
+ });
32
+ }, [token]);
33
+
34
+ return (
35
+ <section>
36
+ <h1>Tasks</h1>
37
+ {error ? <p className="error">{error}</p> : null}
38
+
39
+ <div className="card">
40
+ <table>
41
+ <thead>
42
+ <tr>
43
+ <th>Title</th>
44
+ <th>Status</th>
45
+ <th>Project</th>
46
+ </tr>
47
+ </thead>
48
+ <tbody>
49
+ {tasks.map((task) => (
50
+ <tr key={task.id}>
51
+ <td>{task.title}</td>
52
+ <td>{task.status}</td>
53
+ <td>{task.project_id}</td>
54
+ </tr>
55
+ ))}
56
+ </tbody>
57
+ </table>
58
+ </div>
59
+
60
+ <p>
61
+ Back to <Link to="/organizations">organizations</Link>.
62
+ </p>
63
+ </section>
64
+ );
65
+ }
@@ -1,14 +1,17 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "target": "ES2022",
4
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
4
5
  "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "strict": true,
7
6
  "skipLibCheck": true,
8
- "noEmit": true,
7
+ "moduleResolution": "bundler",
9
8
  "allowImportingTsExtensions": true,
10
- "types": ["bun"],
11
- "lib": ["ES2022", "DOM"]
9
+ "isolatedModules": true,
10
+ "moduleDetection": "force",
11
+ "noEmit": true,
12
+ "jsx": "react-jsx",
13
+ "strict": true,
14
+ "types": ["bun"]
12
15
  },
13
- "include": ["src/**/*.ts"]
16
+ "include": ["src", "build.ts", "dev-server.ts", "bun-env.d.ts"]
14
17
  }
@@ -11,9 +11,9 @@
11
11
  "check": "tsc --noEmit"
12
12
  },
13
13
  "dependencies": {
14
- "@getstrata/bootstrap": "^0.2.53",
14
+ "@getstrata/bootstrap": "^0.4.2",
15
15
  "@getstrata/cli": "^0.2.0",
16
- "@getstrata/core": "^0.5.62"
16
+ "@getstrata/core": "^0.7.4"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/bun": "^1.4.0",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@getstrata/starter",
3
- "version": "0.1.4",
4
- "description": "Scaffold a new Strata app bun create strata",
3
+ "version": "0.1.9",
4
+ "description": "Interactive create-strata wizard. Choose each layer; one database engine.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -17,7 +17,7 @@
17
17
  "README.md"
18
18
  ],
19
19
  "scripts": {
20
- "build": "bun build cli.ts --outdir dist --target bun && cp -r templates dist/templates",
20
+ "build": "bun build cli.ts --outdir dist --target bun && rm -rf dist/templates && mkdir -p dist/templates/overlays && cp -r templates/. dist/templates && cp -r ../../templates/scaffold/. dist/templates/overlays",
21
21
  "prepublishOnly": "bun run build"
22
22
  },
23
23
  "publishConfig": {
@@ -1,19 +0,0 @@
1
- DATABASE_URL=postgresql://postgres:postgres@localhost:5432/{{PROJECT_NAME}}
2
- PORT=3000
3
- APP_URL=http://localhost:3000
4
- APP_ENV=local
5
- APP_NAME={{PROJECT_NAME}}
6
- FRONTEND_MODE=api
7
- TENANCY_DRIVER=none
8
-
9
- # Required in production when FRONTEND_MODE=server-htmx
10
- # SESSION_SECRET=
11
-
12
- # Set only behind a trusted reverse proxy
13
- # TRUST_FORWARDED_FOR=true
14
-
15
- # Required in production to expose GET /metrics
16
- # METRICS_TOKEN=
17
-
18
- # Production should set FEATURE_PUBLIC_READS=false
19
- # FEATURE_PUBLIC_READS=false
@@ -1,14 +0,0 @@
1
- services:
2
- postgres:
3
- image: postgres:16-alpine
4
- environment:
5
- POSTGRES_USER: postgres
6
- POSTGRES_PASSWORD: postgres
7
- POSTGRES_DB: {{PROJECT_NAME}}
8
- ports:
9
- - "5432:5432"
10
- volumes:
11
- - pgdata:/var/lib/postgresql/data
12
-
13
- volumes:
14
- pgdata:
@@ -1,22 +0,0 @@
1
- {
2
- "name": "{{PROJECT_NAME}}",
3
- "version": "0.1.0",
4
- "private": true,
5
- "type": "module",
6
- "scripts": {
7
- "dev": "strata dev",
8
- "start": "strata start",
9
- "db:migrate": "strata migrate",
10
- "db:fresh": "strata migrate:fresh",
11
- "check": "tsc --noEmit"
12
- },
13
- "dependencies": {
14
- "@getstrata/bootstrap": "^0.2.53",
15
- "@getstrata/cli": "^0.2.0",
16
- "@getstrata/core": "^0.5.62"
17
- },
18
- "devDependencies": {
19
- "@types/bun": "^1.4.0",
20
- "typescript": "^5.9.2"
21
- }
22
- }
@@ -1,29 +0,0 @@
1
- :root {
2
- color-scheme: light dark;
3
- font-family: system-ui, sans-serif;
4
- line-height: 1.5;
5
- }
6
-
7
- body {
8
- margin: 0;
9
- padding: 0 1.5rem 2rem;
10
- max-width: 48rem;
11
- }
12
-
13
- .site-header {
14
- padding: 1rem 0;
15
- }
16
-
17
- .brand {
18
- font-weight: 700;
19
- text-decoration: none;
20
- color: inherit;
21
- }
22
-
23
- .section h1 {
24
- margin-top: 0;
25
- }
26
-
27
- code {
28
- font-size: 0.9em;
29
- }
@@ -1,18 +0,0 @@
1
- export interface AppConfig {
2
- port: number;
3
- appUrl: string;
4
- databaseUrl: string;
5
- }
6
-
7
- export function loadConfig(): AppConfig {
8
- const databaseUrl = process.env.DATABASE_URL;
9
- if (!databaseUrl) {
10
- throw new Error("DATABASE_URL is required");
11
- }
12
-
13
- return {
14
- port: Number(process.env.PORT ?? 3000),
15
- appUrl: process.env.APP_URL ?? "http://localhost:3000",
16
- databaseUrl,
17
- };
18
- }