# Kyrox: a coding-first AI assistant (SaaS)

Next.js (App Router) + Upstash Redis token ledger + Stripe subscriptions, backed by a
local `llama-server` running Qwen 2.5 Coder 7B Instruct. Rename the product anywhere by
changing `NEXT_PUBLIC_BRAND_NAME` in `.env.local` — nothing else in the code hardcodes the name.

Languages (18): TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust, PHP, Ruby, Swift, Kotlin, React, HTML, CSS, SQL, Bash.
To add one, extend `LANGUAGES` in `lib/plans.ts` and add its entries to `components/CodeBlock.tsx` (and `lib/demo.ts` for demo mode).

## The assistant

Kyrox is a coding-first, general-purpose assistant. The system prompt lives in `lib/llama.ts`: elite
software-engineering skills (write, debug, review, explain), plus help with any other topic, direct answers
with no filler, code always in fenced blocks. Replies are stored and shown as Markdown (`components/Markdown.tsx`),
and code blocks are highlighted for any language tag (`components/CodeBlock.tsx`). The language picker
defaults to **Auto**.

The quality of the answers depends on the model you serve. For stronger general answers set `LLAMA_MODEL` and load a
general instruct model (e.g. Qwen2.5-Instruct) or a larger Qwen2.5-Coder.

## Plans

| Plan | Tokens | Window | Notes |
|---|---|---|---|
| FREE (default) | 1,000 | 12 hours | Replies capped at 600 tokens (and to what is left), prompts up to 600 characters |
| PRO ($10/mo) | 5,000 | 12 hours | |
| MAX ($20/mo) | 15,000 | 12 hours | |

All plans use the same 12-hour window. Every new account starts on FREE; there is no "no plan" state. FREE has its own ledger key
(`ledger:free:<user>`), so upgrading starts a fresh paid window. The limits live in `lib/plans.ts`.

## Demo mode (no Redis, Stripe or model server)

```bash
npm install
npm run demo        # same as DEMO_MODE=1 next dev
```

Open http://localhost:3000. Accounts, chats and usage live in memory (`lib/redis.ts`) and disappear when the
server restarts. Replies are canned samples (`lib/demo.ts`), clearly labelled in the first line of the code.
Upgrading from the pricing dialog switches plan instantly with no payment, and "Manage" returns you to FREE.
Never set `DEMO_MODE=1` on a real deployment.

## UI structure and flow

New visitor: `/` (Start) → `/register` → `/welcome` (2.2s interstitial) → `/chat`.
Returning visitor: `/` (Start) → `/login` → `/chat`.
Any of `/login`, `/register`, `/chat` redirect an already-authenticated visitor straight
to `/chat`; `/chat` redirects a signed-out visitor to `/login`.

- `/` — Start/landing page. Unauthenticated only; two CTAs (`Create free account`, `Sign in`).
- `/register`, `/login` — dedicated full-page forms (not modals), sharing `components/AuthForm.tsx`
  with a fixed `mode` prop and a link across to the other page.
- `/welcome` — short post-registration interstitial, then auto-redirects to `/chat`.
- `/chat` — the product. Desktop: a collapsible sidebar (`New chat`, search, conversations grouped by
  day, live usage gauge, theme toggle, sign out). Mobile: the same sidebar as a drawer. An empty chat
  shows a greeting, a centered composer and four starter prompts; once you send, the composer docks to
  the bottom and new code streams in line by line.
- Theme: light/dark via CSS variables in `app/globals.css` (colors are Tailwind tokens, so there are
  no `dark:` variants to maintain). The choice is saved in `localStorage` and applied before first paint.
- `components/Mark.tsx` is a small four-point spark built from SVG primitives (not copied artwork),
  used as the brand mark; `app/icon.svg` is the favicon.
- `components/CodeBlock.tsx` renders highlighted code with copy/download; `components/chat/*` holds the
  sidebar, composer, message list and pricing modal.

## Chat history (persisted, not just in-memory)

`lib/conversations.ts` stores conversations in Redis per user:

- `conv:list:{userId}` — a sorted set of conversation ids, scored by last-updated time.
- `conv:meta:{userId}:{id}` — `{ id, title, createdAt, updatedAt }`. The title is auto-set
  from the first user message (truncated to 60 chars) the first time a reply lands.
- `conv:messages:{userId}:{id}` — the full message array for that conversation.

`app/api/generate/route.ts` accepts an optional `conversationId`. If it's missing, or it
doesn't belong to the caller, a new conversation is created server-side and its id is
returned in the response so the client can pick it up. Every successful exchange is
appended via `appendExchange`, which also bumps the conversation to the top of the list.

`app/api/conversations/route.ts` (list, create) and `app/api/conversations/[id]/route.ts`
(read one conversation's messages, delete) back the sidebar.

**Still stateless toward the model itself:** the chat history is for the user's own
navigation (switching between past conversations), but `/api/generate` still sends only
the current prompt to the model — earlier turns in the same conversation are not replayed
as context. Add that to `app/api/generate/route.ts` (read `getConversation`'s messages and
include them in the `messages` array sent to the inference server) if you want real
multi-turn memory; it will also roughly multiply token cost by the conversation length.

## Setup

1. **Install**
   ```bash
   npm install
   cp .env.local.example .env.local
   openssl rand -base64 48   # paste into JWT_SECRET
   ```
2. **Upstash**: create a Redis database, copy the REST URL and token into `.env.local`.
3. **Stripe**
   - Create two recurring prices: $10/month and $20/month. Put the IDs in `STRIPE_PRICE_PRO` / `STRIPE_PRICE_MAX`.
   - Enable Stripe Tax (Checkout uses `automatic_tax`) and the Customer Portal (Settings > Billing).
   - Add a webhook endpoint `https://YOUR_DOMAIN/api/webhooks/stripe` for `checkout.session.completed`,
     `customer.subscription.created`, `customer.subscription.updated`, `customer.subscription.deleted`.
     Copy the signing secret into `STRIPE_WEBHOOK_SECRET`.
   - Local testing: `stripe listen --forward-to localhost:3000/api/webhooks/stripe`
4. **Model**: download `qwen2.5-coder-7b-instruct-q4_k_m.gguf`, then run
   ```bash
   llama-server -m qwen2.5-coder-7b-instruct-q4_k_m.gguf --host 127.0.0.1 --port 8080 -c 8192 -np 2 -t 4
   ```
5. **Run**: `npm run dev`, open http://localhost:3000, register, and subscribe with a Stripe test card (4242 4242 4242 4242).

## Deploying to Oracle Cloud (Always Free ARM)

- Run **both** llama-server and the Next.js app on the same VM. `127.0.0.1:8080` only exists on that machine,
  so **Vercel cannot reach it**. Use Vercel only if you put the model behind a private tunnel, which
  contradicts the localhost-isolation layer.
- Build: `npm ci && npm run build`. `npm start` binds to 127.0.0.1:3000; only Nginx faces the internet.
- Copy `deploy/*.service` to `/etc/systemd/system/`, `deploy/nginx.conf` to Nginx, get certificates with
  `certbot --nginx`, then `systemctl enable --now llama-server kyrox`.
- Open only ports 80/443 in the Oracle security list and the VM firewall. Never 8080 or 3000.
- Set `APP_URL` to your final https origin (it drives the CSRF origin check and Stripe redirects).

## Where the 6 security layers live

| Layer | Implementation |
|---|---|
| 1. Localhost AI isolation | `lib/llama.ts` rejects non-loopback URLs; systemd/Nginx never expose 8080 |
| 2. Server-side ledger | `lib/ratelimit.ts` (atomic Lua on Redis), billed on `usage.total_tokens` |
| 3. JWT auth | `lib/auth.ts`: HS256 pinned, `httpOnly` + `secure` + `sameSite=lax` cookie |
| 4. Stripe signatures | `app/api/webhooks/stripe/route.ts`: raw body + `constructEvent`, event-ID dedupe |
| 5. Input sanitization | `lib/sanitize.ts`: 4,000-char cap, strips ChatML tokens, control and bidi characters |
| 6. Headers and CORS | `next.config.mjs` (CSP, nosniff, DENY) and `middleware.ts` (same-origin only) |

## Deliberate deviations from the spec

- **`/api/generate` ignores `plan` and `userId` in the request body.** Trusting them would let any user
  send `{"plan":"MAX"}` and get 15,000 tokens for free. Identity comes from the JWT; the plan comes from
  Redis, written only by the verified webhook.
- **Reserve-then-settle billing.** An estimate is reserved atomically before inference, then replaced
  with the exact `usage.total_tokens` (refunded fully if the model errors). This stops parallel requests
  from overspending. A single request can slightly overshoot the limit; the next one is then blocked.
- **The window starts on first spend** and expires 43,200 s (12 h) later, rather than on fixed clock boundaries.
- **Extra Stripe events** (`created`, `deleted`) so cancelled subscriptions lose access.
- **Prompt injection cannot be fully prevented by filtering.** Defense is structural: fixed system prompt,
  user text only in the `user` role, no tools, and output rendered as inert highlighted text.
- **CSP keeps `'unsafe-inline'` for scripts** because Next.js needs it without a nonce setup.

## Known limits

- A 7B model on 4 ARM cores generates roughly a few tokens per second; expect 1 to 3 minute responses
  for long outputs. `-np 2` allows two concurrent generations. Add a queue if you expect more traffic.
- Accounts are email and password stored in Redis (scrypt-hashed). There is no email verification or
  password reset yet. Swap in NextAuth or a hosted provider if you need those.
- This code was written without network access, so `npm install`, `npm run typecheck` and `npm run build`
  have not been run. Run them first and fix any small type or version mismatches.
