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
12 changes: 12 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.build
.next
.turbo
.git
node_modules
apps/web/.next
apps/web/node_modules
packages/lexicons/node_modules
*.log
.env
.env.*
!.env.example
15 changes: 15 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
ATELIER_ENV=development
ATELIER_PUBLIC_URL=http://localhost:3000
ATELIER_API_URL=http://localhost:8080
NEXT_PUBLIC_ATELIER_API_URL=http://localhost:8080
ATELIER_DB_URL=postgres://atelier:atelier@localhost:5432/atelier_mail
ATELIER_KV_BACKEND=postgres
ATELIER_KMS_PROVIDER=local-dev
ATELIER_TOKEN_KEK_ID=local-dev-kek
ATELIER_ATPROTO_CLIENT_ID=http://localhost:3000/client-metadata.json
ATELIER_ATPROTO_REDIRECT_URI=http://localhost:3000/auth/callback
ATELIER_GMAIL_CLIENT_ID=
ATELIER_GMAIL_CLIENT_SECRET=
ATELIER_GMAIL_PUBSUB_AUDIENCE=
ATELIER_MCP_ENABLED=true
ATELIER_MCP_MYCONTEXTPROTOCOL_URL=
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:
pull_request:
push:
branches: [main, dev]

jobs:
web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
- run: bun install --frozen-lockfile
- run: bun run typecheck
- run: bun run lint
- run: bun run test
- run: bun run build

swift:
runs-on: macos-15
steps:
- uses: actions/checkout@v5
- run: swift test
working-directory: services/api
21 changes: 21 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.DS_Store
.env
.env.local
.env.*.local
node_modules
.next
dist
coverage
.turbo
.bun-install-cache
*.tsbuildinfo
.build
.swiftpm
DerivedData
xcuserdata
*.xcodeproj
*.xcworkspace
*.sqlite
*.sqlite-shm
*.sqlite-wal
infra/.data
56 changes: 56 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// swift-tools-version: 6.0
import PackageDescription

let package = Package(
name: "AtelierMailWorkspace",
platforms: [
.macOS(.v14)
],
products: [
.executable(name: "AtelierAPI", targets: ["AtelierAPI"]),
.library(name: "AtelierCore", targets: ["AtelierCore"]),
.library(name: "AtelierATProto", targets: ["AtelierATProto"]),
.library(name: "AtelierMCP", targets: ["AtelierMCP"]),
.library(name: "AtelierSync", targets: ["AtelierSync"]),
.library(name: "AtelierPlatform", targets: ["AtelierPlatform"]),
.library(name: "AtelierMail", targets: ["AtelierMail"])
],
targets: [
.target(name: "AtelierCore", path: "packages/swift/AtelierCore/Sources/AtelierCore"),
.target(
name: "AtelierATProto",
dependencies: ["AtelierCore"],
path: "packages/swift/AtelierATProto/Sources/AtelierATProto"
),
.target(
name: "AtelierMCP",
dependencies: ["AtelierCore"],
path: "packages/swift/AtelierMCP/Sources/AtelierMCP"
),
.target(
name: "AtelierSync",
dependencies: ["AtelierCore"],
path: "packages/swift/AtelierSync/Sources/AtelierSync"
),
.target(
name: "AtelierPlatform",
dependencies: ["AtelierCore", "AtelierATProto", "AtelierMCP"],
path: "packages/swift/AtelierPlatform/Sources/AtelierPlatform"
),
.target(
name: "AtelierMail",
dependencies: ["AtelierCore", "AtelierPlatform", "AtelierSync"],
path: "packages/swift/AtelierMail/Sources/AtelierMail"
),
.executableTarget(
name: "AtelierAPI",
dependencies: ["AtelierCore", "AtelierPlatform", "AtelierMail", "AtelierMCP"],
path: "services/api/Sources/AtelierAPI"
),
.testTarget(
name: "AtelierAPITests",
dependencies: ["AtelierAPI", "AtelierCore", "AtelierPlatform", "AtelierMail", "AtelierMCP"],
path: "services/api/Tests/AtelierAPITests"
)
]
)
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Atelier Mail

Atelier Mail is the first application scaffold for the Atelier Work suite at `atelierwork.space`.

The MVP is a SaaS-first mail client with:

- ATProto OAuth identity with DID as the primary account key.
- Server-side Gmail, JMAP, and IMAP provider access.
- A local normalized mail domain model backed by PostgreSQL.
- Private permissioned Lexicon concepts stored in Atelier KV until ATProto Permissioned Data is finalized.
- MCP hooks for agent access through an explicit grant model.
- A Next.js/shadcn/Tailwind web client inspired by the scalable Social Wire shell and the warmer Skej interaction style.

## Structure

- `apps/web` - Next.js 16 app shell for `mail.atelierwork.space`.
- `services/api` - Swift/Hummingbird service scaffold.
- `packages/swift` - extraction-ready Swift package boundaries.
- `packages/lexicons` - `space.atelierwork.*` draft schemas and validation tests.
- `infra` - local Docker Compose and operational scaffolding.
- `docs` - architecture and implementation notes.

## Local Commands

```bash
bun install
bun run verify
```

Swift checks:

```bash
DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer swift test
```

Full verification:

```bash
bun run verify:all
```

Local deploy smoke:

```bash
docker compose -f infra/docker-compose.yml up --build
```

See `docs/deployment.md` for deployment environment and process details.
21 changes: 21 additions & 0 deletions apps/web/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
FROM oven/bun:1.3.14-alpine AS deps
WORKDIR /app
COPY package.json bun.lock bunfig.toml ./
COPY apps/web/package.json apps/web/package.json
COPY packages/lexicons/package.json packages/lexicons/package.json
RUN bun install --frozen-lockfile

FROM deps AS builder
WORKDIR /app
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN bun run --filter './apps/web' build

FROM oven/bun:1.3.14-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
COPY --from=builder /app ./
EXPOSE 3000
CMD ["bun", "run", "--filter", "./apps/web", "start"]
25 changes: 25 additions & 0 deletions apps/web/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-nova",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}
6 changes: 6 additions & 0 deletions apps/web/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";

const eslintConfig = [...nextVitals, ...nextTs];

export default eslintConfig;
6 changes: 6 additions & 0 deletions apps/web/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
16 changes: 16 additions & 0 deletions apps/web/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { NextConfig } from "next";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const appDir = dirname(fileURLToPath(import.meta.url));
const workspaceRoot = resolve(appDir, "../..");

const nextConfig: NextConfig = {
typedRoutes: true,
devIndicators: false,
turbopack: {
root: workspaceRoot,
},
};

export default nextConfig;
48 changes: 48 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "@atelier-mail/web",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --webpack",
"dev:turbo": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "eslint --max-warnings 0",
"test": "bun test --preload ./src/test/setup.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@atelierwork/lexicons": "workspace:*",
"@tanstack/react-virtual": "^3.13.24",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.14.0",
"next": "16.2.6",
"react": "19.2.4",
"react-dom": "19.2.4",
"tailwind-merge": "^3.6.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@testing-library/react": "^16.3.2",
"@types/bun": "^1.3.4",
"@types/jsdom": "^28.0.3",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.6",
"jsdom": "^27.2.0",
"playwright": "^1.61.1",
"tailwindcss": "^4",
"typescript": "^5"
},
"ignoreScripts": [
"sharp",
"unrs-resolver"
],
"trustedDependencies": [
"sharp",
"unrs-resolver"
]
}
7 changes: 7 additions & 0 deletions apps/web/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};

export default config;
46 changes: 46 additions & 0 deletions apps/web/scripts/capture-screenshots.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { chromium } from "playwright";

const browser = await chromium.launch({ headless: true });

try {
const desktop = await browser.newPage({
viewport: { width: 1440, height: 1000 },
deviceScaleFactor: 1,
});
await desktop.goto("http://localhost:3000", { waitUntil: "networkidle" });
await desktop.screenshot({
path: "../../.codex-desktop.png",
fullPage: true,
});

await desktop.getByLabel("Switch to dark mode").click();
await desktop.waitForTimeout(150);
await desktop.screenshot({
path: "../../.codex-desktop-dark.png",
fullPage: true,
});

const mobile = await browser.newPage({
viewport: { width: 390, height: 844 },
isMobile: true,
});
await mobile.goto("http://localhost:3000", { waitUntil: "networkidle" });
await mobile.screenshot({
path: "../../.codex-mobile.png",
fullPage: true,
});

const result = {
desktopTitle: await desktop.locator("text=Atelier Mail").first().isVisible(),
threadVisible: await desktop
.locator("text=Permissioned KV model for linked mail threads")
.first()
.isVisible(),
mobileSearch: await mobile.locator('input[aria-label="Search"]').isVisible(),
darkMode: await desktop.locator("html.dark").count(),
};

console.log(JSON.stringify(result, null, 2));
} finally {
await browser.close();
}
Loading