GlassKit

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:

LocationUsed forExample
Repo rootTop-level deployable surfaceapp/ (glasses webapp), companion/ (Next.js site)
packages/*Reusable libraries imported by surfacespackages/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 npm
  • scripts: at minimum dev, build, typecheck. Optionally lint + test. Names must match the tasks declared in turbo.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:

pnpm-workspace.yaml
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 admin

Initialize package.json

admin/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 3001 because companion's already on :3000. Pick a free port.
  • The workspace:* dep on @glasskit/backend gives you api imports 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 only

Turbo 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

admin/app/layout.tsx
"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/format

Initialize package.json

packages/format/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:

pnpm-workspace.yaml
catalog:
  vitest: ^3.0.0
  zod: ^3.24.0
  "@t3-oss/env-core": ^0.13.0
  "@t3-oss/env-nextjs": ^0.13.0

Workspaces 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 existing scripts/setup.ts pattern). No workspace needed.
  • GitHub Actions: .github/workflows/, not a workspace.
  • Documentation source: content/docs/*.mdx lives 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 under companion/app/ or a component under companion/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.json with @glasskit/<name> name, private: true, scripts matching turbo's task pipeline
  • tsconfig.json extending a sibling's config
  • env.ts if the surface needs env vars
  • .env.example documenting 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 run pnpm install from the root
  • If using shared deps already in the catalog (vitest, zod, @t3-oss/env-*), reference as "<pkg>": "catalog:" instead of pinning a version

On this page