# Tinyforge

Self-hosted deployment platform with a web dashboard. Deploy Docker containers from registries with zero-downtime blue-green strategy, host static sites and Deno APIs directly from Git repositories, and manage reverse proxy configuration — all from a single binary.

## Features

### Container Deployments

- **Registry polling** and **webhook receiver** for automatic deployments
- **Blue-green deploys** with health checks and automatic rollback
- **Multi-stage projects** (dev, staging, prod) with tag pattern matching
- **Real-time deploy logs** via SSE streaming

### Branch Preview Environments

Get an isolated, throwaway deploy for every feature branch:

- Add a **branch pattern** (e.g. `feat/*`) to a workload's **git trigger** (Triggers panel → git trigger → *Branch pattern*).
- Pushing to any branch matching the pattern deploys an **isolated per-branch preview** — a child workload that inherits the source config, served at a **slug-prefixed subdomain** (`feat-login-app.example.com`) so previews never collide with each other or the main deploy.
- Previews are **automatically torn down** when the branch is deleted upstream.
- Manage live previews from the app's **Preview environments** panel (`/apps/[id]`): open each branch's URL or tear it down manually. A torn-down preview is recreated on the next push to its branch.

### Static Sites

Deploy static sites and Deno-powered APIs directly from Git repositories:

- **Git providers**: Gitea/Forgejo, GitHub, and GitLab (public and private repos)
- **Static mode**: Serves HTML/CSS/JS via nginx container
- **Deno mode**: Full-stack with TypeScript API backend + static frontend — API routes are auto-discovered from `/api` folder using a naming convention (`API_get_users`, `API_post_items`, etc.)
- **Markdown rendering**: Optionally converts `.md` files to styled HTML
- **Branch & folder picker**: Select any branch and subfolder as the deployment root
- **Auto-sync**: Trigger redeployment on push or tag events, or manually
- **Per-site secrets**: Encrypted environment variables injected at runtime

### Infrastructure

- **NPM / Traefik integration** for automatic reverse proxy and SSL configuration
- **Cloudflare DNS** sync for automatic DNS record management
- **Volume management**: Create, browse, upload, and download Docker volumes
- **Stale container cleanup**: Detect and remove unused containers
- **Image management**: List and prune unused Docker images
- **Database backups**: Scheduled and manual backups with one-click restore
- **Config export/import**: YAML-based seed configuration for reproducible setups

### Auth & Security

- **Local auth** with bcrypt password hashing
- **OIDC/SSO** support for single sign-on
- **Encrypted credential storage** (AES-256-GCM)
- **Role-based access**: Admin and user roles

## Prerequisites

- Docker with Docker Compose
- A Docker network for deployed containers (e.g. `staging-net`)
- Nginx Proxy Manager (optional, for automatic proxy configuration)
- Wildcard DNS pointing to your server (for subdomain-based routing)

## Quick Start

1. **Create the Docker network** (containers will be attached to this):

   ```bash
   docker network create staging-net
   ```

2. **Create a `.env` file** (see `.env.example`):

   ```bash
   cp .env.example .env
   # Edit .env and set ENCRYPTION_KEY and ADMIN_PASSWORD
   # Generate a key: openssl rand -hex 32
   ```

3. **Start Tinyforge**:

   ```bash
   docker compose up -d
   ```

4. **Open the dashboard** at `http://localhost:8080` and log in with `admin` / your `ADMIN_PASSWORD`.

## Configuration

### Environment Variables

| Variable           | Required            | Description                                                                      |
| ------------------ | ------------------- | -------------------------------------------------------------------------------- |
| `ENCRYPTION_KEY`   | Yes                 | AES-256 key for encrypting stored credentials. Use `openssl rand -hex 32`        |
| `ADMIN_PASSWORD`   | Yes (first launch)  | Password for the default admin user                                              |
| `SEED_FILE`        | No                  | Path to YAML seed config (default: `./tinyforge.yaml`)                           |
| `DATA_DIR`         | No                  | SQLite database directory (default: `./data`)                                    |
| `LISTEN_ADDR`      | No                  | HTTP listen address (default: `:8080`)                                           |
| `NPM_URL`          | No                  | Override NPM API URL (otherwise uses value from settings)                        |
| `POLLING_INTERVAL` | No                  | Registry polling interval, Go duration string e.g. `5m` (default from settings) |

### Seed Config

On first launch, Tinyforge imports a YAML seed file to pre-configure registries, projects, and settings. See `tinyforge.example.yaml` for the full format.

### Webhook Integration

After setup, find your webhook URL at **Settings > Webhook URL** in the dashboard. Configure your CI/CD (Gitea Actions, GitHub Actions) to POST to this URL on image push:

```bash
curl -X POST https://your-domain/api/webhook/<secret> \
  -H "Content-Type: application/json" \
  -d '{"image": "registry.example.com/org/app:v1.2.3"}'
```

### OIDC Setup

1. Go to **Settings > Auth** in the dashboard
2. Switch auth mode to **OIDC**
3. Enter your provider's Issuer URL, Client ID, and Client Secret
4. Set the Redirect URL to `https://your-domain/api/auth/oidc/callback`

## CLI

`tinyforge` is a terminal client for driving a server from the shell, built on the same HTTP API as the web UI.

### Build

```bash
go build -o tinyforge ./cmd/cli      # ./tinyforge (tinyforge.exe on Windows)
```

### Usage

```bash
# Log in once — caches a 24h token in ~/.tinyforge/config.json (mode 0600)
tinyforge login --base-url http://localhost:8090
# ...or non-interactively (no password echo / shell-history leak):
TINYFORGE_PASSWORD=… tinyforge login --base-url http://localhost:8090 --user admin

tinyforge apps                              # list apps + container state
tinyforge deploy my-app                     # deploy and wait for completion
tinyforge deploy my-app --ref v1.2.3 --note "hotfix"
tinyforge logs my-app -f                    # follow logs (Ctrl-C to stop)
tinyforge status                            # server health + current user
tinyforge status my-app                     # one app's containers
tinyforge logout                            # revoke + clear the cached token
```

### Server & token resolution

| Setting  | Flag         | Env               | Default                  |
| -------- | ------------ | ----------------- | ------------------------ |
| Base URL | `--base-url` | `TINYFORGE_URL`   | `http://localhost:8080`  |
| Token    | `--token`    | `TINYFORGE_TOKEN` | cached by `login`        |
| Config   | `--config`   | `TINYFORGE_CONFIG`| `~/.tinyforge/config.json` |

### Notes

- Login returns a **24h JWT** — there is no long-lived API token yet, so unattended use re-logs in when the token expires. `deploy` / `stop` / `start` require an **admin** account.
- The token is sent as an `Authorization: Bearer` header (never placed in the URL) and the config file is written with `0600` permissions.

## Development

```bash
# Build frontend
cd web && npm install && npm run build && cd ..

# Run backend (requires ENCRYPTION_KEY and ADMIN_PASSWORD env vars)
go run ./cmd/server

# Or use Make
make build
make dev
```

## Architecture

```text
CI/Registry --> Webhook/Poller --> Deployer --> Docker + NPM
                                      |
Git Repo ----> Static Sites -------> Docker + NPM
                                      |
                                  Event Bus --> SSE --> Web Dashboard
```

- **Backend**: Go 1.24, chi router, SQLite (pure Go), Docker SDK
- **Frontend**: SvelteKit 2, Tailwind CSS 4, TypeScript
- **Deployment**: Single binary with embedded SPA, multi-stage Dockerfile
