Skip to content
Merged
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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ jobs:
run: pnpm build
env:
NEXT_PUBLIC_CONVEX_URL: https://terrific-llama-923.convex.cloud
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ vars.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }}

- name: Upload build artifacts
uses: actions/upload-artifact@v4
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ jobs:
run: pnpm build
env:
NEXT_PUBLIC_CONVEX_URL: https://terrific-llama-923.convex.cloud
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ vars.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }}

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ jobs:
run: pnpm build
env:
NEXT_PUBLIC_CONVEX_URL: https://terrific-llama-923.convex.cloud
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ vars.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }}

release:
name: Create GitHub Release
Expand Down
91 changes: 90 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,94 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

---

## [0.3.0] - 2026-06-30

### Overview

This release integrates **Clerk** as the authentication provider for `apps/web`,
wired directly into the Convex real-time backend via `ConvexProviderWithClerk`.
Authentication is enforced at both the UI layer (Next.js 16 proxy middleware) and
the backend layer (Convex mutation identity checks).

---

### Added

#### Clerk authentication — `apps/web`

- **`@clerk/nextjs ^7.5.9`** added to `apps/web` dependencies
- **`app/layout.tsx`** — `ClerkProvider` wraps the entire application, providing
Clerk session context to all pages and components
- **`proxy.ts`** (Next.js 16 middleware filename) — `clerkMiddleware()` protects all
routes by default; public routes `/sign-in(.*)` and `/sign-up(.*)` are exempt
- **`app/(auth)/layout.tsx`** — centered layout for all auth pages
- **`app/(auth)/sign-in/[[...sign-in]]/page.tsx`** — Clerk hosted `<SignIn />` component
with catch-all routing for multi-step sign-in flows
- **`app/(auth)/sign-up/[[...sign-up]]/page.tsx`** — Clerk hosted `<SignUp />` component
with catch-all routing for multi-step sign-up flows
- **`app/page.tsx`** — `<Authenticated>` / `<Unauthenticated>` guards from Convex;
authenticated view shows `<UserButton />` and the Add user mutation button;
unauthenticated view shows `<SignInButton />`

#### Convex + Clerk session bridging

- **`components/theme-provider.tsx`** — replaced bare `ConvexProvider` with
`ConvexProviderWithClerk` (from `convex/react-clerk`), passing `useAuth` from
`@clerk/nextjs` so Convex automatically includes the active Clerk JWT in all
function calls. Removed dead `ThemeHotkey`, `isTypingTarget`, `NextThemesProvider`,
and `useTheme` code that was leftover from the previous provider setup.

#### Convex backend auth hardening — `packages/backend`

- **`convex/auth.config.ts`** — Clerk JWT provider configuration; reads
`CLERK_JWT_ISSUER_DOMAIN` from the Convex Dashboard environment. Uses
`/// <reference types="node" />` because this file runs in Node.js (Convex CLI),
not the V8 function isolate runtime.
- **`convex/users.ts`** — `add` mutation now calls `ctx.auth.getUserIdentity()` and
throws `"Not Authenticated"` for unauthenticated callers, preventing anonymous
writes to the users table.
- **`package.json`** — added `@types/node ^20.19.41` dev dependency

---

### Fixed

- **`packages/backend/tsconfig.json`** — removed deprecated `baseUrl` compiler option.
In TypeScript 5+, `paths` does not require `baseUrl` to be set; the option was
flagged as deprecated in TS 5.0 and will stop functioning in TS 7.0.

---

### Changed

- **`apps/widget/components/theme-provider.tsx`** — removed dead `ThemeHotkey`,
`isTypingTarget`, `NextThemesProvider`, and `useTheme` code, matching the cleanup
done in `apps/web`.

#### CI/tooling

- `ci.yml`, `release.yml`, `codeql.yml` — `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` added
to the build step environment, reading from the `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`
GitHub repository variable. This prevents `ClerkProvider` from throwing during
`next build` in CI environments where `.env.local` is absent.

---

### Technical Decisions

- **`ConvexProviderWithClerk` over manual token injection** — Convex's official Clerk
integration handles token refresh, expiry, and re-auth automatically. Manual token
passing would require re-implementing this logic.
- **Middleware-level route protection** — protecting routes at the `proxy.ts` layer
means unauthenticated users are redirected before any page code runs, not just
hidden by client-side conditionals.
- **`ctx.auth.getUserIdentity()` in mutations** — server-side auth checks are the last
line of defence; even if middleware is bypassed, mutations reject unauthenticated calls.
- **`proxy.ts` (not `middleware.ts`)** — Next.js 16 renamed the middleware file from
`middleware.ts` to `proxy.ts`. The code API is identical; only the filename changed.

---

## [0.2.0] - 2026-06-29

### Overview
Expand Down Expand Up @@ -249,7 +337,8 @@ Initial release of **Echo** — an enterprise-grade full-stack monorepo platform

---

[Unreleased]: https://github.com/RISHII7/echo/compare/v0.2.0...HEAD
[Unreleased]: https://github.com/RISHII7/echo/compare/v0.3.0...HEAD
[0.3.0]: https://github.com/RISHII7/echo/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/RISHII7/echo/compare/v0.1.1...v0.2.0
[0.1.1]: https://github.com/RISHII7/echo/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/RISHII7/echo/releases/tag/v0.1.0
27 changes: 20 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
[![Turborepo](https://img.shields.io/badge/Turborepo-2-EF4444?logo=turborepo)](https://turbo.build)
[![Tailwind CSS](https://img.shields.io/badge/Tailwind-4-38BDF8?logo=tailwindcss)](https://tailwindcss.com)
[![Convex](https://img.shields.io/badge/Convex-backend-EE342F?logo=convex)](https://convex.dev)
[![Clerk](https://img.shields.io/badge/Clerk-auth-6C47FF?logo=clerk)](https://clerk.com)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md)

[Report Bug](https://github.com/RISHII7/echo/issues/new?template=bug_report.yml) · [Request Feature](https://github.com/RISHII7/echo/issues/new?template=feature_request.yml) · [Documentation](https://github.com/RISHII7/echo/wiki)
Expand Down Expand Up @@ -57,7 +58,8 @@ Echo is a production-ready, full-stack monorepo platform engineered for enterpri
## Features

- **Monorepo Architecture** — Turborepo with remote caching, task pipelines, and workspace dependency graph
- **Real-Time Backend** — Convex reactive database with live queries and server mutations
- **Authentication** — Clerk with hosted sign-in/sign-up UI, session management, and middleware-level route protection
- **Real-Time Backend** — Convex reactive database with live queries and server mutations, bridged to Clerk sessions
- **Type Safety** — End-to-end TypeScript with strict mode and generated API types across all packages
- **Design System** — Shared UI component library built on shadcn/ui and Radix primitives
- **Performance** — Next.js Turbopack, React Server Components, and Tailwind CSS v4
Expand Down Expand Up @@ -96,6 +98,7 @@ The monorepo uses a **workspace dependency graph** where apps consume packages,
| UI Library | React 19 |
| Styling | Tailwind CSS 4 |
| Components | shadcn/ui + Radix UI |
| Auth | Clerk |
| Backend | Convex (real-time DB) |
| Icons | Lucide React |
| Monorepo | Turborepo 2 |
Expand Down Expand Up @@ -133,20 +136,30 @@ pnpm dev

### Environment Variables

Create `apps/web/.env.local` and `apps/widget/.env.local` with the following:
Create `apps/web/.env.local`:

| Variable | Description | Required |
| ------------------------ | --------------------------------- | -------- |
| `NEXT_PUBLIC_CONVEX_URL` | Convex deployment URL | Yes |
| `NEXT_PUBLIC_APP_URL` | Public URL of the web application | No |
| Variable | Description | Required |
| ----------------------------------- | ------------------------------------- | -------- |
| `NEXT_PUBLIC_CONVEX_URL` | Convex deployment URL | Yes |
| `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` | Clerk publishable key (`pk_test_...`) | Yes |
| `CLERK_SECRET_KEY` | Clerk secret key (`sk_test_...`) | Yes |

To get your `NEXT_PUBLIC_CONVEX_URL`, run `pnpm --filter backend dev` and copy the deployment URL from the Convex dashboard, or from `packages/backend/.env.local` after the first `convex dev` run.
Create `packages/backend/.env.local` (auto-generated by `convex dev`):

| Variable | Description | Required |
| ------------------------- | ------------------------------------ | -------- |
| `CONVEX_DEPLOYMENT` | Convex deployment identifier | Yes |
| `CLERK_JWT_ISSUER_DOMAIN` | Clerk JWT issuer URL for Convex auth | Yes |

Comment on lines +147 to 153

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Clarify the CLERK_JWT_ISSUER_DOMAIN setup note.

The table states that packages/backend/.env.local is "auto-generated by convex dev", but CLERK_JWT_ISSUER_DOMAIN is not auto-generated — it must be manually configured from the Clerk Dashboard. Only CONVEX_DEPLOYMENT is automatically created by convex dev. This could mislead developers setting up the backend.

📝 Suggested clarification
-Create `packages/backend/.env.local` (auto-generated by `convex dev`):
+Create `packages/backend/.env.local` (`CONVEX_DEPLOYMENT` is auto-generated by `convex dev`):
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Create `packages/backend/.env.local` (auto-generated by `convex dev`):
| Variable | Description | Required |
| ------------------------- | ------------------------------------ | -------- |
| `CONVEX_DEPLOYMENT` | Convex deployment identifier | Yes |
| `CLERK_JWT_ISSUER_DOMAIN` | Clerk JWT issuer URL for Convex auth | Yes |
Create `packages/backend/.env.local` (`CONVEX_DEPLOYMENT` is auto-generated by `convex dev`):
| Variable | Description | Required |
| ------------------------- | ------------------------------------ | -------- |
| `CONVEX_DEPLOYMENT` | Convex deployment identifier | Yes |
| `CLERK_JWT_ISSUER_DOMAIN` | Clerk JWT issuer URL for Convex auth | Yes |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 147 - 153, Clarify the setup note in README around
the `CLERK_JWT_ISSUER_DOMAIN` entry so it is clear that only `CONVEX_DEPLOYMENT`
is auto-generated by `convex dev`, while `CLERK_JWT_ISSUER_DOMAIN` must be
manually configured from the Clerk Dashboard. Update the text near the
`.env.local` table to avoid implying both values are generated automatically,
and keep the wording aligned with the `CONVEX_DEPLOYMENT` and
`CLERK_JWT_ISSUER_DOMAIN` variable descriptions.

```bash
# apps/web/.env.local
NEXT_PUBLIC_CONVEX_URL=https://<your-deployment>.convex.cloud
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
```

Get your Clerk keys from the [Clerk Dashboard](https://dashboard.clerk.com) → **API Keys**.

---

## Development
Expand Down
9 changes: 9 additions & 0 deletions apps/web/app/(auth)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const Layout = ({ children }: { children: React.ReactNode }) => {
return (
<div className="flex h-full min-h-screen min-w-screen flex-col items-center justify-center">
{children}
</div>
)
}

export default Layout
7 changes: 7 additions & 0 deletions apps/web/app/(auth)/sign-in/[[...sign-in]]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { SignIn } from "@clerk/nextjs"

const Page = () => {
return <SignIn />
}

export default Page
7 changes: 7 additions & 0 deletions apps/web/app/(auth)/sign-up/[[...sign-up]]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { SignUp } from "@clerk/nextjs"

const Page = () => {
return <SignUp />
}

export default Page
11 changes: 8 additions & 3 deletions apps/web/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { ClerkProvider } from "@clerk/nextjs"
import { Geist, Geist_Mono } from "next/font/google"

import "@workspace/ui/globals.css"
import { ThemeProvider } from "@/components/theme-provider"
import { cn } from "@workspace/ui/lib/utils"

import { ThemeProvider } from "@/components/theme-provider"

import "@workspace/ui/globals.css"

const geist = Geist({ subsets: ["latin"], variable: "--font-sans" })

const fontMono = Geist_Mono({
Expand All @@ -28,7 +31,9 @@ export default function RootLayout({
)}
>
<body>
<ThemeProvider>{children}</ThemeProvider>
<ClerkProvider>
<ThemeProvider>{children}</ThemeProvider>
</ClerkProvider>
</body>
</html>
)
Expand Down
31 changes: 23 additions & 8 deletions apps/web/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
"use client"

import { useMutation, useQuery } from "convex/react"
import {
useMutation,
useQuery,
Authenticated,
Unauthenticated,
} from "convex/react"
import { api } from "@workspace/backend/_generated/api"
import { Button } from "@workspace/ui/components/button"
import { SignInButton, UserButton } from "@clerk/nextjs"

export default function Page() {
const users = useQuery(api.users.getMany)
const addUser = useMutation(api.users.add)
return (
<div className="flex min-h-svh flex-col items-center justify-center">
<p> apps/web</p>
<Button onClick={() => addUser()}>Add</Button>
<div className="mx-auto w-full max-w-sm">
{JSON.stringify(users, null, 2)}
</div>
</div>
<>
<Authenticated>
<div className="flex min-h-svh flex-col items-center justify-center">
<p> apps/web</p>
<UserButton />
<Button onClick={() => addUser()}>Add</Button>
<div className="mx-auto w-full max-w-sm">
{JSON.stringify(users, null, 2)}
</div>
</div>
</Authenticated>
<Unauthenticated>
<p>Must be signed In</p>
<SignInButton>Sign In!</SignInButton>
</Unauthenticated>
</>
)
}
61 changes: 10 additions & 51 deletions apps/web/components/theme-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,22 @@
"use client"

import * as React from "react"
import { ConvexProvider, ConvexReactClient } from "convex/react"
import { ThemeProvider as NextThemesProvider, useTheme } from "next-themes"
import { ConvexReactClient } from "convex/react"
import { ConvexProviderWithClerk } from "convex/react-clerk"

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL || "")
import { useAuth } from "@clerk/nextjs"

function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <ConvexProvider client={convex}>{children}</ConvexProvider>
if (!process.env.NEXT_PUBLIC_CONVEX_URL) {
throw new Error("Missing NEXT_PUBLIC_CONVEX_URL in your .env file")
}

function isTypingTarget(target: EventTarget | null) {
if (!(target instanceof HTMLElement)) {
return false
}
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL)

function ThemeProvider({ children }: { children: React.ReactNode }) {
return (
target.isContentEditable ||
target.tagName === "INPUT" ||
target.tagName === "TEXTAREA" ||
target.tagName === "SELECT"
<ConvexProviderWithClerk client={convex} useAuth={useAuth}>
{children}
</ConvexProviderWithClerk>
)
}

function ThemeHotkey() {
const { resolvedTheme, setTheme } = useTheme()

React.useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
if (event.defaultPrevented || event.repeat) {
return
}

if (event.metaKey || event.ctrlKey || event.altKey) {
return
}

if (event.key.toLowerCase() !== "d") {
return
}

if (isTypingTarget(event.target)) {
return
}

setTheme(resolvedTheme === "dark" ? "light" : "dark")
}

window.addEventListener("keydown", onKeyDown)

return () => {
window.removeEventListener("keydown", onKeyDown)
}
}, [resolvedTheme, setTheme])

return null
}

export { ThemeProvider }
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@clerk/nextjs": "^7.5.9",
"@workspace/backend": "workspace:*",
"@workspace/math": "workspace:*",
"@workspace/ui": "workspace:*",
Expand Down
18 changes: 18 additions & 0 deletions apps/web/proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"

const isPublicRoute = createRouteMatcher(["/sign-in(.*)", "/sign-up(.*)"])

export default clerkMiddleware(async (auth, req) => {
if (!isPublicRoute(req)) {
await auth.protect()
}
Comment on lines +3 to +8

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make / public or the guest homepage can never render.

apps/web/app/page.tsx now has an <Unauthenticated> branch with a sign-in CTA, but this middleware still protects /. Unauthenticated visitors will be redirected by Line 7 before that branch can render, so the new signed-out landing experience is unreachable.

Suggested fix
-const isPublicRoute = createRouteMatcher(["/sign-in(.*)", "/sign-up(.*)"])
+const isPublicRoute = createRouteMatcher([
+  "/",
+  "/sign-in(.*)",
+  "/sign-up(.*)",
+])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const isPublicRoute = createRouteMatcher(["/sign-in(.*)", "/sign-up(.*)"])
export default clerkMiddleware(async (auth, req) => {
if (!isPublicRoute(req)) {
await auth.protect()
}
const isPublicRoute = createRouteMatcher([
"/",
"/sign-in(.*)",
"/sign-up(.*)",
])
export default clerkMiddleware(async (auth, req) => {
if (!isPublicRoute(req)) {
await auth.protect()
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/proxy.ts` around lines 3 - 8, The middleware in clerkMiddleware is
still protecting the root route, which prevents the new unauthenticated landing
page in app/page.tsx from ever rendering. Update the isPublicRoute matcher in
proxy.ts to include "/" so auth.protect() is skipped for the homepage, while
keeping the existing sign-in and sign-up public routes unchanged.

})

export const config = {
matcher: [
// Skip Next.js internals and all static files, unless found in search params
"/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
// Always run for API routes
"/(api|trpc)(.*)",
],
}
Loading
Loading