Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Brave Search API key (required) — https://api.search.brave.com/
BRAVE_API_KEY=

# OpenAI API key for GPT-4o-mini synthesis (required) — https://platform.openai.com/
OPENAI_API_KEY=

# Cache TTL in seconds (optional, default: 300)
CACHE_TTL_SECONDS=300

# Server port (optional, default: 3000)
PORT=3000
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install dependencies
run: bun install

- name: Run tests
run: bun test

- name: Type check
run: bun run tsc -- --noEmit
137 changes: 137 additions & 0 deletions DEPLOY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
![CI](https://github.com/langoustine69/queryx/actions/workflows/ci.yml/badge.svg)

# Deploying queryx to Railway

## Prerequisites

- [Railway account](https://railway.app/) (free tier works)
- [Railway CLI](https://docs.railway.app/develop/cli) installed: `npm install -g @railway/cli`
- [Docker](https://www.docker.com/) installed (for local testing)
- `BRAVE_API_KEY` from [Brave Search API](https://api.search.brave.com/)
- `OPENAI_API_KEY` from [OpenAI Platform](https://platform.openai.com/)

---

## Option A: Railway CLI Deployment

```bash
# 1. Log in to Railway
railway login

# 2. Create a new Railway project
railway new

# 3. Link to the project (if already created)
railway link

# 4. Set required environment variables
railway variables set BRAVE_API_KEY=your_brave_api_key_here
railway variables set OPENAI_API_KEY=your_openai_api_key_here

# Optional variables
railway variables set CACHE_TTL_SECONDS=300
railway variables set PORT=3000

# 5. Deploy
railway up
```

---

## Option B: Dashboard Deployment

1. Go to [railway.app](https://railway.app/) and create a new project.
2. Click **New Service** and choose **GitHub Repo**.
3. Connect your GitHub account and select the `queryx` repository.
4. Railway will auto-detect the `Dockerfile` and use it for builds.
5. Navigate to the **Variables** tab in your service settings.
6. Add the following environment variables:

| Variable | Required | Description |
|---|---|---|
| `BRAVE_API_KEY` | Yes | Brave Search API key |
| `OPENAI_API_KEY` | Yes | OpenAI API key |
| `CACHE_TTL_SECONDS` | No (default: 300) | Cache TTL in seconds |
| `PORT` | No (default: 3000) | Server port |

7. Click **Deploy** — Railway will build the Docker image and deploy it.

---

## Custom Domain Setup

1. In the Railway dashboard, go to your service settings.
2. Click **Settings** > **Networking** > **Custom Domain**.
3. Enter your domain name and follow the DNS configuration instructions.
4. Railway provides a CNAME record to add to your DNS provider.
5. Once DNS propagates, your service will be accessible at your custom domain.

---

## Health Check Verification

After deployment, verify the service is healthy:

```bash
# Replace with your Railway app URL
curl https://your-app.railway.app/health
```

Expected response:

```json
{"status":"ok"}
```

A successful response confirms the service is running correctly.

---

## Rollback

To roll back to a previous deployment:

```bash
# List recent deployments
railway deployments

# Roll back to the previous deployment
railway rollback
```

Or roll back from the Railway dashboard by clicking **Deployments** and selecting a previous deploy.

---

## Local Docker Test

Test your Docker build locally before deploying:

```bash
# Copy the example env file and fill in your values
cp .env.example .env
# Edit .env with your actual API keys

# Build the Docker image
docker build -t queryx .

# Run the container
docker run -p 3000:3000 --env-file .env queryx

# Verify locally
curl http://localhost:3000/health
```

---

## Smoke Test

Run the included smoke test script against any deployment:

```bash
# Test local instance
bash scripts/smoke-test.sh http://localhost:3000

# Test production
bash scripts/smoke-test.sh https://your-app.railway.app
```
14 changes: 14 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM oven/bun:1 AS base
WORKDIR /app

COPY package.json bun.lock ./
RUN bun install --frozen-lockfile

COPY . .

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1

CMD ["bun", "run", "src/index.ts"]
12 changes: 12 additions & 0 deletions railway.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"$schema": "https://railway.app/railway.schema.json",
"build": {
"builder": "DOCKERFILE"
},
"deploy": {
"healthcheckPath": "/health",
"healthcheckTimeout": 10,
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 3
}
}
57 changes: 57 additions & 0 deletions scripts/smoke-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
set -euo pipefail

BASE_URL="${1:-http://localhost:3000}"
PASS=0
FAIL=0

check() {
local name="$1"
local result="$2"
if [ "$result" = "ok" ]; then
echo " PASS: $name"
PASS=$((PASS + 1))
else
echo " FAIL: $name — $result"
FAIL=$((FAIL + 1))
fi
}

echo "Smoke test: $BASE_URL"
echo "---"

# Test /health returns 200
status=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/health")
if [ "$status" = "200" ]; then
check "/health returns 200" "ok"
else
check "/health returns 200" "got $status"
fi

# Test /health returns {"status":"ok"}
body=$(curl -s "$BASE_URL/health")
if echo "$body" | grep -q '"status"'; then
check "/health body has status field" "ok"
else
check "/health body has status field" "got: $body"
fi

# Test /v1/search returns 402 without payment
status=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/v1/search?q=test")
if [ "$status" = "402" ]; then
check "/v1/search returns 402 without payment" "ok"
else
check "/v1/search returns 402 without payment" "got $status"
fi

# Test 402 response has x402 headers
headers=$(curl -sI "$BASE_URL/v1/search?q=test")
if echo "$headers" | grep -qi "x-payment\|402\|payment-required"; then
check "402 response has payment headers" "ok"
else
check "402 response has payment headers" "missing payment headers"
fi

echo "---"
echo "Results: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ] && exit 0 || exit 1