Adding a new workspace
Add a mobile app, admin panel, or shared package to the GlassKit monorepo without breaking the existing turbo + pnpm + Convex wiring.
GlassKit ships four workspaces: app/, companion/,
packages/glasses-ui/, packages/backend/. Eventually you'll
want to add a fifth: a mobile app, an admin panel, a shared
component library, an internal CLI. This doc walks the
conventions so the new workspace plays nicely with everything
already wired.
The two kinds of workspace
GlassKit's structure treats them differently:
| Location | Used for | Example |
|---|---|---|
| Repo root | Top-level deployable surface | app/ (glasses webapp), companion/ (Next.js site) |
packages/* | Reusable libraries imported by surfaces | packages/glasses-ui, packages/backend |
Pick by intent:
Building another surface
A mobile app, admin web app, Slack bot: anything users interact with. Add at the repo root alongside app/ and companion/.
Extracting reusable code
A design system, domain-logic library: anything two or more surfaces share. Add under packages/.
Both kinds are pnpm workspaces; the only difference is discoverability + the convention of "what people expect to find where."
The contract every workspace satisfies
To play with turbo + pnpm + the existing tooling, your new workspace needs:
A package.json with:
name: namespace as@glasskit/<name>(consistent with the rest of the monorepo)private: true: these aren't published to npmscripts: at minimumdev,build,typecheck. Optionallylint+test. Names must match the tasks declared inturbo.json(currently:dev,build,lint,typecheck,test).
A tsconfig.json (most likely extending one of the existing
ones).
Cross-workspace imports use "workspace:*" in dependencies:
"dependencies": {
"@glasskit/glasses-ui": "workspace:*",
"@glasskit/backend": "workspace:*"
}The workspace globs in pnpm-workspace.yaml already cover both
locations:
packages:
- app
- companion
- packages/*Add a directory at the right location and pnpm picks it up on the
next pnpm install.
Walkthrough: adding an admin web app
Concrete example. You want an internal admin panel: Next.js, deploys to a separate Vercel project, reads the same Convex backend.
Create the workspace
mkdir admin
cd adminInitialize package.json
{
"name": "@glasskit/admin",
"version": "0.1.0",
"private": true,
"description": "Internal admin panel — user / billing / feature-flag tools.",
"scripts": {
"dev": "next dev --port 3001",
"build": "next build",
"start": "next start --port 3001",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@clerk/nextjs": "^7.0.0",
"@glasskit/backend": "workspace:*",
"convex": "^1.39.0",
"next": "16.2.6",
"react": "19.2.4",
"react-dom": "19.2.4"
},
"devDependencies": {
"@types/node": "^22.10.0",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"typescript": "^5.7.3"
}
}Notes
dev --port 3001because companion's already on:3000. Pick a free port.- The
workspace:*dep on@glasskit/backendgives youapiimports for free. - Match the React / Next versions to companion to avoid duplicate installs in the pnpm content-addressable store.
Add a tsconfig
Copy companion/tsconfig.json as a starting point: same
moduleResolution + paths config. Tweak the @/* alias if you
want.
Run pnpm install from the repo root
pnpm picks up the new workspace, resolves the workspace:*
reference to packages/backend, links node_modules.
Use the new workspace
pnpm --filter @glasskit/admin dev # runs admin only
pnpm dev # runs everything (admin included)
pnpm --filter @glasskit/admin build # builds admin onlyTurbo discovers the new workspace automatically: it reads
pnpm-workspace.yaml + each workspace's package.json for
scripts that match its task pipeline. No turbo.json change
needed.
Wire to Convex
"use client";
import { ClerkProvider } from "@clerk/nextjs";
import { ConvexProviderWithClerk } from "convex/react-clerk";
import { ConvexReactClient } from "convex/react";
import { useAuth } from "@clerk/nextjs";
const convex = new ConvexReactClient(env.NEXT_PUBLIC_CONVEX_URL);
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<ClerkProvider>
<ConvexProviderWithClerk client={convex} useAuth={useAuth}>
{children}
</ConvexProviderWithClerk>
</ClerkProvider>
);
}The Convex client uses the same NEXT_PUBLIC_CONVEX_URL as
companion: same backend, same Clerk JWT verification, same Stripe
entitlements. The admin panel is just another authenticated reader.
Add your own env validation
Create admin/env.ts following the
env validation pattern. Bootstrap-import it from
admin/next.config.ts.
Deploy
Create a second Vercel project pointing at the same repo, with
Root Directory = admin/. Turbo + pnpm-lock auto-detected.
Mirrors how the companion + glasses-app are deployed.
Walkthrough: adding a shared package
Concrete example. You realize both app/ and companion/ need
the same date-formatting helpers. Extract into packages/format/.
Create the package
mkdir -p packages/format/src
cd packages/formatInitialize package.json
{
"name": "@glasskit/format",
"version": "0.1.0",
"private": true,
"type": "module",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"typecheck": "tsc --noEmit",
"test": "vitest run"
},
"devDependencies": {
"typescript": "^5.7.3",
"vitest": "catalog:"
}
}Match the shape of packages/glasses-ui/package.json: same
exports field, same conventions. Use "vitest": "catalog:" so the
version is centrally managed via pnpm-workspace.yaml's catalog
(see Quickstart for the catalog pattern).
Add to dependents
// app/package.json
"dependencies": {
"@glasskit/format": "workspace:*"
}
// companion/package.json
"dependencies": {
"@glasskit/format": "workspace:*"
}pnpm install from the repo root resolves both.
Use
// anywhere in app/ or companion/
import { formatDate } from "@glasskit/format";The shared package's source is in packages/format/src/. Both
consumers import directly from there. There's no build step for
shared packages because every consumer is TypeScript-aware.
How turbo handles your new workspace
Two things to know:
Task discovery is automatic
Turbo reads pnpm-workspace.yaml + each workspace's package.json
scripts. If your new workspace declares a build script,
pnpm turbo run build includes it. Same for typecheck, lint,
test, dev. No turbo.json changes needed.
Dependency-aware ordering
turbo.json declares "dependsOn": ["^build"] for the build
task. This means turbo builds shared packages (packages/*)
BEFORE the surfaces that depend on them. If your new shared
package needs to be built before consumers, declare its own
build script and the dependency chain takes care of the rest.
For workspaces that have no real build step (TypeScript-only
shared packages), omit the build script. Turbo will still
typecheck them in dependency order via the typecheck task.
Workspace dependencies + pnpm catalog
pnpm-workspace.yaml has a catalog: section that pins versions
shared across workspaces:
catalog:
vitest: ^3.0.0
zod: ^3.24.0
"@t3-oss/env-core": ^0.13.0
"@t3-oss/env-nextjs": ^0.13.0Workspaces reference these as "<pkg>": "catalog:":
{
"devDependencies": {
"vitest": "catalog:"
}
}Bumping vitest to ^4.0.0 in the catalog updates every
workspace simultaneously. Use the catalog for any cross-workspace
dependency where version drift would cause type-mismatch pain
(zod, vitest, @t3-oss/env-* are the current entries).
What NOT to add as a workspace
- One-off scripts: keep these in
scripts/at the repo root (matches the existingscripts/setup.tspattern). No workspace needed. - GitHub Actions:
.github/workflows/, not a workspace. - Documentation source:
content/docs/*.mdxlives in the marketing repo, not the boilerplate. - Things that are really just
app/features: adding a workspace per route would be overkill. Add a route undercompanion/app/or a component undercompanion/components/.
The workspace boundary is for independently-deployable surfaces or genuinely shared code. Not for organizational nesting.
A complete checklist for a new surface
- Directory at the right location (repo root for surfaces,
packages/for libraries) -
package.jsonwith@glasskit/<name>name,private: true, scripts matching turbo's task pipeline -
tsconfig.jsonextending a sibling's config -
env.tsif the surface needs env vars -
.env.exampledocumenting those vars - Bootstrap-import the env file from your config
(
next.config.ts,vite.config.ts, etc.) - If deploying to Vercel, add a "Deploy to Vercel" button to the boilerplate README
- If sharing code, add
workspace:*deps in the consumers'package.jsons and runpnpm installfrom the root - If using shared deps already in the catalog (
vitest,zod,@t3-oss/env-*), reference as"<pkg>": "catalog:"instead of pinning a version