7 Commits

Author SHA1 Message Date
ramvignesh-b d28903c611 refactor: move healthcheck configuration from docker-compose to Dockerfile
CI / build (pull_request) Successful in 1m19s
2026-05-12 01:21:27 +05:30
ramvignesh-b a4f3ea7837 refactor: replace REDIS_URL with individual host and port configuration variables 2026-05-12 01:05:27 +05:30
ramvignesh-b 7c4ef8a51c chore: update healthcheck to use bun fetch instead of curl
CI / build (push) Successful in 22s
2026-05-12 00:57:23 +05:30
ramvignesh-b 2eab4b92cc test: implement redis unavailibility health check
CI / build (push) Successful in 1m27s
2026-05-11 23:59:01 +05:30
ramvignesh-b 51502055db feat: add healthcheck configuration
CI / build (push) Failing after 24s
2026-05-11 23:44:40 +05:30
ramvignesh-b 553d9647c2 chore: add production start script and update readme
CI / build (push) Successful in 21s
2026-05-11 17:44:19 +05:30
me b954ce5f72 ci: integrate tests workflow
CI / build (push) Successful in 22s
Reviewed-on: #2
2026-05-11 12:01:48 +00:00
10 changed files with 85 additions and 39 deletions
+3 -4
View File
@@ -1,6 +1,5 @@
# Core Server Configuration APP_PORT=3000
PORT=3000
API_KEY=your_secret_api_key_here API_KEY=your_secret_api_key_here
# Redis Configuration (Use redis://redis:6379 for Docker) REDIS_HOST=redis
REDIS_URL=redis://localhost:6379 REDIS_PORT=6379
+4 -1
View File
@@ -11,4 +11,7 @@ ENV NODE_ENV=production
USER bun USER bun
EXPOSE 3000 EXPOSE 3000
CMD ["bun", "run", "src/index.ts"] HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD bun -e "fetch('http://localhost:3000/health').then(res => res.ok ? process.exit(0) : process.exit(1)).catch(e => process.exit(1))"
CMD ["bun", "run", "start"]
+41 -25
View File
@@ -20,47 +20,63 @@
- **Styling**: Tailwind CSS & DaisyUI - **Styling**: Tailwind CSS & DaisyUI
- **Schema & Validation**: Zod - **Schema & Validation**: Zod
## Quick Start ## Getting Started
toknd can be deployed either as a containerized service or self-hosted directly on your hardware.
### 1. Environment Setup ### 1. Environment Setup
Clone the repository and create your environment file: Clone the repository and create your environment file:
```bash ```bash
cp .env.example .env cp .env.example .env
``` ```
Ensure you define a strong `API_KEY` in your `.env`. Define a strong `API_KEY` and ensure `REDIS_URL` points to a valid Redis instance.
### 2. Local Development (with Auto-Watch) ### 2. Choose Deployment Method
We use a Docker Compose override system to enable hot-reloading locally:
```bash
podman compose up --build
```
*Note: This mounts your ./src directory into the container and uses bun --hot to restart on any code changes.*
### 3. Production Deployment #### Option A: Containerized (Recommended)
For production, only the core docker-compose.yml is used: This is the easiest way to get up and running, as it bundles the application and a Redis instance together.
```bash
docker compose up -d --build - **Development (with Hot-Reload)**:
``` ```bash
podman compose up --build
```
- **Production**:
```bash
docker compose up -d --build
```
#### Option B: Self-Hosting (Bare Metal)
Ideal for lightweight deployments or custom environments where you already have Bun and Redis.
1. **Install Dependencies**:
```bash
bun install
```
2. **Start the Server**:
- **Development**: `bun run dev` (with hot-reload)
- **Production**: `bun run start`
*Note: Ensure your Redis server is running and accessible via the `REDIS_URL` in your `.env`.*
---
## API Reference ## API Reference
All protected endpoints require an Authorization header: toknd provides a built-in **Scalar API Reference** that allows you to explore and test all endpoints directly from your browser.
- **Interactive UI**: [http://localhost:3000/api](http://localhost:3000/api) (or `/docs`)
- **OpenAPI Spec (JSON)**: [http://localhost:3000/doc](http://localhost:3000/doc)
All protected endpoints require a Bearer token in the `Authorization` header:
`Authorization: Bearer <your_master_api_key>` `Authorization: Bearer <your_master_api_key>`
### Token Brokerage ### Core Concepts
- **Get Valid Token**: `GET /api/token/:provider` - **Token Brokerage**: Automated access token retrieval and background refreshes for all configured providers.
- Returns a valid access token. Automatically triggers a refresh if the current one is expired. - **Provider Management**: Register and manage OAuth2 providers via the Dashboard or the configuration API.
- **Registry Status**: `GET /api/status`
- Returns the connectivity and refresh status of all configured providers.
### Authentication Flow
1. **Initiate**: `GET /auth/:provider/login`
2. **Callback**: `GET /auth/callback` (Handled internally by toknd)
## Dashboard ## Dashboard
Access the toknd dashboard at: Access the **toknd** dashboard at:
`http://localhost:3000/app` `http://localhost:3000/app`
Authenticate the registry using your Master API Key to manage your providers and view live token status. The dashboard allows you to manage provider configurations, view live token statuses, and manually trigger refreshes. Authenticate using your **Master API Key**.
--- ---
+5 -2
View File
@@ -3,9 +3,10 @@ services:
build: . build: .
restart: always restart: always
ports: ports:
- "${PORT:-3000}:3000" - "${APP_PORT:-3000}:3000"
environment: environment:
- REDIS_URL=redis://redis:6379 - REDIS_HOST=redis
- REDIS_PORT=6379
- API_KEY=${API_KEY} - API_KEY=${API_KEY}
depends_on: depends_on:
- redis - redis
@@ -13,6 +14,8 @@ services:
redis: redis:
image: redis:alpine image: redis:alpine
restart: always restart: always
ports:
- "${REDIS_PORT:-6379}:6379"
volumes: volumes:
- redis-data:/data - redis-data:/data
+1
View File
@@ -4,6 +4,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "bun run --hot src/index.ts", "dev": "bun run --hot src/index.ts",
"start": "bun src/index.ts",
"test": "bun test", "test": "bun test",
"lint": "bunx @biomejs/biome check src", "lint": "bunx @biomejs/biome check src",
"check-all": "bunx @biomejs/biome check .", "check-all": "bunx @biomejs/biome check .",
+3 -2
View File
@@ -1,8 +1,9 @@
import { z } from "zod"; import { z } from "zod";
const configSchema = z.object({ const configSchema = z.object({
PORT: z.string().default("3000"), APP_PORT: z.string().default("3000"),
REDIS_URL: z.string(), REDIS_HOST: z.string().default("redis"),
REDIS_PORT: z.coerce.number().default(6379),
API_KEY: z.string(), API_KEY: z.string(),
}); });
+4 -1
View File
@@ -1,4 +1,7 @@
import { Redis } from "ioredis"; import { Redis } from "ioredis";
import { config } from "../config"; import { config } from "../config";
export const redis = new Redis(config.REDIS_URL); export const redis = new Redis({
host: config.REDIS_HOST,
port: config.REDIS_PORT,
});
+8 -2
View File
@@ -3,6 +3,7 @@ import { Scalar } from "@scalar/hono-api-reference";
import { logger } from "hono/logger"; import { logger } from "hono/logger";
import { prettyJSON } from "hono/pretty-json"; import { prettyJSON } from "hono/pretty-json";
import { config } from "./config"; import { config } from "./config";
import { redis } from "./core/RedisClient";
import { apiRoutes } from "./routes/api"; import { apiRoutes } from "./routes/api";
import { authRoutes } from "./routes/auth"; import { authRoutes } from "./routes/auth";
import { configRoutes } from "./routes/config"; import { configRoutes } from "./routes/config";
@@ -63,11 +64,16 @@ app.onError((err, c) => {
return c.json({ error: "Internal Server Error", message: err.message }, 500); return c.json({ error: "Internal Server Error", message: err.message }, 500);
}); });
app.get("/health", (c) => c.json({ status: "ok" })); app.get("/health", async (c) => {
if (redis.status !== "ready") {
return c.json({ status: "error", message: "Redis down", redis: redis.status }, 503);
}
return c.json({ status: "ok" });
});
export { app }; export { app };
export default { export default {
port: Number.parseInt(config.PORT, 10), port: Number.parseInt(config.APP_PORT, 10),
fetch: app.fetch, fetch: app.fetch,
}; };
+11
View File
@@ -35,6 +35,17 @@ describe("API Integration", () => {
expect(body.status).toBe("ok"); expect(body.status).toBe("ok");
}); });
it("should return 503 for health check if redis is down", async () => {
redis.status = "connecting";
const res = await app.request("/health");
const body = await res.json();
expect(res.status).toBe(503);
expect(body.status).toBe("error");
expect(body.redis).toBe("connecting");
});
it("should return 200 for status with valid API Key", async () => { it("should return 200 for status with valid API Key", async () => {
redis.keys.mockReturnValue(Promise.resolve(["config:trakt"])); redis.keys.mockReturnValue(Promise.resolve(["config:trakt"]));
redis.get.mockImplementation((key) => { redis.get.mockImplementation((key) => {
+5 -2
View File
@@ -2,14 +2,17 @@ import { mock } from "bun:test";
// Global test setup to stub environment variables // Global test setup to stub environment variables
process.env.API_KEY = "test-api-key"; process.env.API_KEY = "test-api-key";
process.env.REDIS_URL = "redis://localhost:6379"; process.env.REDIS_HOST = "localhost";
process.env.PORT = "3000"; process.env.REDIS_PORT = "6379";
process.env.APP_PORT = "3000";
// Global Redis mock // Global Redis mock
mock.module("../src/core/RedisClient", () => ({ mock.module("../src/core/RedisClient", () => ({
redis: { redis: {
status: "ready",
get: mock(() => Promise.resolve(null)), get: mock(() => Promise.resolve(null)),
set: mock(() => Promise.resolve()), set: mock(() => Promise.resolve()),
keys: mock(() => Promise.resolve([])), keys: mock(() => Promise.resolve([])),
on: mock(() => {}),
}, },
})); }));