Skip to content

Latest commit

 

History

History
1545 lines (1273 loc) · 54 KB

File metadata and controls

1545 lines (1273 loc) · 54 KB

FLOWSPACE — Complete Product Specification

Version 1.0 | Senior Developer PRD | Open Source + SaaS


0. PRODUCT VISION

FlowSpace is a unified project management platform that blends the structural power of ClickUp, the document flexibility of Notion, and the clean aesthetic of Linear — built open source from day one with a cloud SaaS layer on top.

Core belief: Teams don't fail because of missing features. They fail because tools are either too rigid (Jira) or too free-form (Notion). FlowSpace gives you structure where you need it and freedom where you want it.

Tagline: Work that flows.


1. TECH STACK

Frontend

Layer Technology Version Why We Use It
Framework Next.js 16.2 (latest stable) The current production release. v16 ships Turbopack as the default bundler (5–10× faster Fast Refresh vs webpack), Cache Components via PPR for instant navigation, and proxy.ts replacing middleware.ts for an explicit network boundary. v16.2 adds a stable Adapter API for multi-provider deployment and Agent DevTools for AI-assisted debugging.
UI Components shadcn/ui + Radix UI Latest shadcn/ui is not a library — it's a registry of copy-paste components built on Radix primitives. You own the code, so there's no black-box dependency to fight. Radix handles all the accessibility (ARIA, keyboard nav, focus management) so you never have to.
Styling Tailwind CSS v4 v4 rewrites the engine in Rust (Lightning CSS), drops the config file in favour of CSS-native @theme, and is 5× faster to build. Utility-first means design tokens live in one place and every component stays consistent without custom CSS sprawl.
Animation Motion v12 The standalone successor to Framer Motion (same API, smaller bundle). Spring physics, layout animations, and scroll-driven effects. We use it for task panel slide-overs, kanban card drags, and micro-interactions — things CSS alone can't handle smoothly.
Rich Text BlockNote v0.x latest A Notion-style block editor built on TipTap/ProseMirror. Gives us slash commands, drag-to-reorder blocks, collaborative editing hooks, and a clean JSON document model out of the box. Writing this from scratch would cost months.
Drag & Drop dnd-kit v6 The only production-ready DnD library that is accessible (keyboard + screen reader support), works with virtual lists, and handles the complex pointer/touch edge cases that matter in a kanban board. react-beautiful-dnd is unmaintained; dnd-kit is the successor.
State Zustand v5 Minimal client state for UI concerns (sidebar open/closed, active view, selection state). ~1KB, no boilerplate, no context providers. We deliberately keep server state in TanStack Query and reserve Zustand for ephemeral UI state only.
Forms React Hook Form + Zod RHF v7 / Zod v3 RHF is uncontrolled by default so re-renders only happen on blur/submit — critical for large task forms. Zod provides end-to-end type safety: the same schema validates on the frontend and is reused in the tRPC router, so a bad payload is impossible.
Data Fetching TanStack Query v5 Manages all server state: caching, background refetching, optimistic updates, and deduplication. Optimistic mutations are how we make the UI feel instant — the task moves on the board before the server responds, and rolls back silently on error.
Tables TanStack Table v8 Headless table logic (sorting, filtering, pagination, grouping, column visibility) with zero opinion on markup or styling. Pairs with TanStack Virtual for rendering 10,000+ task rows without a performance cliff.
Charts Recharts v2 Declarative React chart library built on D3. Used for dashboard widgets (burndown, workload bar, status donut). Recharts is the right balance of customisability vs setup time for our use case.
Icons Lucide React Latest The community-maintained successor to Feather Icons. ~1,500 consistent SVG icons, tree-shakeable so unused icons don't ship. Every icon in the app comes from one source for visual consistency.
Date date-fns v4 Functional, immutable date utilities. No global state, no mutations, fully tree-shakeable. Used everywhere dates appear: due date pickers, timeline rendering, relative timestamps ("3 hours ago").
Virtualisation TanStack Virtual v3 Row virtualisation for the list and table views. Without it, rendering 5,000+ tasks destroys scroll performance. With it, only ~20 DOM nodes exist at a time regardless of dataset size.

Backend

Layer Technology Version Why We Use It
Runtime Node.js 24 LTS ("Krypton") Node 24 is the current Active LTS line (supported until April 2028). It ships V8 13.x (faster JIT, better memory), stable built-in SQLite, native fetch, and TypeScript type stripping via --experimental-strip-types — meaning we can run .ts files directly without a build step in development. Node 20 reached end of life April 2026.
API Server Hono.js v4 Sub-millisecond cold starts, runs identically on Node, Bun, Deno, Cloudflare Workers, and Vercel Edge. Express-like DX but fully typed. We chose Hono over Express because Express has no native TypeScript support and over Fastify because Hono's middleware model is simpler for our team.
ORM Drizzle ORM v0.x latest The only TypeScript ORM where the query builder output is 1:1 with the SQL it generates — no magic, no surprises. Migrations are plain SQL files you review and commit. Drizzle is actively maintained and has become the de facto choice for new TypeScript projects in 2025–2026.
Database PostgreSQL 18.3 (latest stable) Released September 2025. Key reasons to be on 18 vs 16: native uuidv7() function (perfect for our task IDs — sortable + unique), async I/O subsystem (up to 3× faster sequential scans), virtual generated columns (compute-on-read, no storage overhead), and OAuth 2.0 authentication support. Actively maintained until 2030.
Cache / Queue Redis via Upstash Redis 7.x Used for three purposes: (1) session token cache to avoid DB hits on every request, (2) BullMQ job queue backend, (3) pub/sub for broadcasting realtime events to Socket.io instances. Upstash is serverless Redis — no infra to manage, pay-per-request pricing fits both self-host (swap for local Redis) and SaaS.
Background Jobs BullMQ v5 Reliable, Redis-backed job queue with retries, rate limiting, priorities, and delayed jobs. Used for: sending notification emails, processing file uploads, running automation triggers, and rebuilding search indexes.
File Storage S3-compatible (MinIO self-host / AWS S3 cloud) MinIO is a drop-in S3-compatible server — self-hosters run it locally, our SaaS uses AWS S3. One abstraction, two deployment targets. Files are served via signed URLs (time-limited, no public buckets).
Search Meilisearch v1 Full-text search across tasks, docs, comments, and projects. Sub-100ms results with typo tolerance. Meilisearch is simpler to operate than Elasticsearch and 10× faster to set up. It indexes our data asynchronously via BullMQ jobs on every write.
Realtime Liveblocks v2 Collaborative editing presence (live cursors, who's-viewing-this-doc avatars, typing indicators) in documents. Liveblocks handles the WebSocket infrastructure and CRDT merge logic — building this ourselves would be a 6-month project. Self-hosted alternative: PartyKit.
Auth Better Auth (self-host) / Clerk (SaaS fast path) BA v1 Better Auth is the new open-source standard replacing Lucia Auth (which was deprecated in late 2024). Supports email/password, magic links, OAuth (Google, GitHub), sessions, and 2FA out of the box. Clerk is used as the SaaS fast path — it handles email deliverability, bot protection, and MFA UI for paying customers.
Email React Email + Resend RE v3 React Email lets us build transactional emails as React components (typed, testable, previewed in browser). Resend is the delivery API — high deliverability, simple SDK, generous free tier. Emails are queued via BullMQ so a slow SMTP server never blocks a user action.
WebSockets Socket.io v4 Used for live task updates and notifications (someone assigned you a task, a status changed). Socket.io handles reconnection, room-based broadcasting, and fallback to long-polling for restrictive networks. Rooms map 1:1 to workspace IDs for clean isolation.

Infrastructure / DevOps

Layer Technology Version Why We Use It
Monorepo Nx v21 Full monorepo platform, not just a task runner. Chosen over Turborepo for four specific reasons: (1) nx generate scaffolds new apps, libraries, and components consistently — no contributor invents their own folder structure; (2) @nx/enforce-module-boundaries ESLint rule prevents the open-source core from accidentally importing proprietary SaaS billing code at the lint stage, not just by convention; (3) Nx Agents distributes CI tasks across multiple machines — critical once the test suite grows past 10 minutes; (4) nx graph gives a live visual dependency map, invaluable when onboarding OSS contributors. First-class plugins for Next.js (@nx/next), Node/Hono (@nx/node), Playwright (@nx/playwright), Docker, and Storybook. Remote caching via Nx Cloud (free tier for open-source). Turborepo would be the right call if we were a 2-person team who just wanted fast builds — we're not.
Package Manager pnpm v10 Strict dependency isolation (phantom deps are impossible), content-addressable store saves disk space on CI, and workspaces-native. pnpm is the default choice for all modern TypeScript monorepos in 2026.
Container Docker + Docker Compose Docker 27 / Compose v2 Single-command self-hosting (docker compose up -d). Each service (web, api, postgres, redis, meilisearch, minio) is a container. Reproducible environments across dev, staging, and production.
CI/CD GitHub Actions Free for open-source repos. Runs lint, type-check, unit tests, and E2E tests on every PR. Deploys to staging on merge to main, production on a release tag. Nx Cloud remote cache integrates natively, and Nx Agents can split long test runs across multiple CI machines.
Self-host Deploy Railway / Coolify Railway is the one-click PaaS for self-hosters who want managed infra. Coolify is the self-hosted alternative (runs on your own VPS). Both support Docker Compose deployments — same config, two deployment targets.
SaaS Deploy Vercel (frontend) + Railway (backend) Vercel for Next.js is unbeatable: edge network, ISR, automatic preview URLs per PR, and zero config. Railway hosts the Hono API, PostgreSQL, Redis, and Meilisearch. Both support environment variable management and zero-downtime deploys.
Monitoring OpenTelemetry + Sentry OTel v1 / Sentry v8 OpenTelemetry gives us vendor-neutral traces and metrics. Sentry catches unhandled errors in both the Next.js frontend and Hono backend with full stack traces and source maps. Sentry's Session Replay is invaluable for reproducing user-reported bugs.
Analytics PostHog v3 Open-source product analytics — self-hostable so no data leaves the user's server. Tracks activation, feature usage, funnels, and session recordings. GDPR-compliant by design.
API Docs Scalar + OpenAPI 3.1 Scalar generates a beautiful interactive API reference from our OpenAPI spec. Developers can test endpoints directly in the browser. We generate the OpenAPI spec from our Zod schemas so docs are always in sync with the code.
Testing Vitest + Playwright Vitest v3 / PW v1.5x Vitest for unit and integration tests (100× faster than Jest, native ESM). Playwright for end-to-end tests — it's the only E2E framework that reliably handles Next.js server components, drag-and-drop interactions, and WebSocket-driven UI.

Inspired by Twenty CRM Stack

  • Metadata-driven architecture (like Twenty's object system) for custom fields
  • GraphQL-inspired API layer via tRPC for type-safe end-to-end calls
  • Workspace-based multi-tenancy (same as Twenty's approach)
  • Open source core + cloud SaaS model identical to Twenty's strategy

2. MONOREPO STRUCTURE

flowspace/
├── apps/
│   ├── web/                    # Next.js frontend
│   ├── api/                    # Hono.js backend
│   ├── docs/                   # Documentation site (Nextra)
│   └── desktop/                # Electron wrapper (Phase 3)
├── packages/
│   ├── ui/                     # Shared shadcn component library
│   ├── db/                     # Drizzle schema, migrations, seeds
│   │   ├── schema/             # Table definitions (one file per domain)
│   │   ├── migrations/         # Auto-generated SQL migration files
│   │   ├── seeds/              # Seed scripts (dev | test | demo)
│   │   ├── metadata/           # Metadata engine (object/field registry)
│   │   └── drizzle.config.ts
│   ├── types/                  # Shared TypeScript types
│   ├── auth/                   # Auth utilities
│   ├── email/                  # React Email templates
│   ├── config/                 # ESLint, TS, Tailwind configs
│   └── utils/                  # Shared utility functions
├── e2e/                        # Playwright E2E test suite
│   ├── tests/
│   ├── fixtures/
│   └── playwright.config.ts
├── docker-compose.yml
├── docker-compose.test.yml     # Isolated containers for CI
├── nx.json
└── pnpm-workspace.yaml

3. DATABASE SCHEMA

Core Tables

-- Workspaces (top-level tenant unit)
workspaces
  id uuid PK
  name text
  slug text UNIQUE
  logo_url text
  plan text DEFAULT 'free'  -- free | pro | business | enterprise
  created_at timestamptz
  settings jsonb DEFAULT '{}'

-- Users
users
  id uuid PK
  email text UNIQUE
  name text
  avatar_url text
  created_at timestamptz

-- Workspace members
workspace_members
  workspace_id uuid FK
  user_id uuid FK
  role text  -- owner | admin | member | guest
  joined_at timestamptz
  PRIMARY KEY (workspace_id, user_id)

-- Spaces (like ClickUp Spaces — project groups)
spaces
  id uuid PK
  workspace_id uuid FK
  name text
  color text
  icon text
  is_private boolean DEFAULT false
  created_at timestamptz

-- Projects (inside Spaces)
projects
  id uuid PK
  space_id uuid FK
  name text
  description text
  status text  -- active | archived | on_hold
  color text
  icon text
  start_date date
  due_date date
  created_by uuid FK
  created_at timestamptz
  settings jsonb  -- custom views, default statuses, etc.

-- Statuses (per project)
statuses
  id uuid PK
  project_id uuid FK
  name text
  color text
  type text  -- todo | in_progress | done | cancelled
  position int

-- Tasks
tasks
  id uuid PK
  project_id uuid FK
  parent_id uuid FK NULLABLE  -- subtask support
  title text
  description jsonb  -- BlockNote JSON
  status_id uuid FK
  priority text  -- urgent | high | medium | low | none
  assignee_id uuid FK
  created_by uuid FK
  start_date date
  due_date date
  estimated_hours decimal
  actual_hours decimal
  position decimal  -- fractional indexing for ordering
  tags text[]
  custom_fields jsonb  -- flexible metadata
  created_at timestamptz
  updated_at timestamptz

-- Comments
comments
  id uuid PK
  task_id uuid FK
  user_id uuid FK
  content jsonb  -- rich text
  parent_id uuid FK NULLABLE  -- threaded replies
  created_at timestamptz
  updated_at timestamptz
  reactions jsonb DEFAULT '{}'

-- Documents (Notion-like pages)
documents
  id uuid PK
  workspace_id uuid FK
  project_id uuid FK NULLABLE  -- can be standalone
  parent_id uuid FK NULLABLE   -- nested docs
  title text
  content jsonb  -- BlockNote JSON
  icon text
  cover_url text
  is_published boolean DEFAULT false
  published_token text NULLABLE
  created_by uuid FK
  created_at timestamptz
  updated_at timestamptz

-- Time Entries
time_entries
  id uuid PK
  task_id uuid FK
  user_id uuid FK
  started_at timestamptz
  ended_at timestamptz NULLABLE
  duration_seconds int NULLABLE
  description text
  is_running boolean DEFAULT false

-- Notifications
notifications
  id uuid PK
  user_id uuid FK
  type text
  payload jsonb
  is_read boolean DEFAULT false
  created_at timestamptz

-- Activity Log
activities
  id uuid PK
  workspace_id uuid FK
  entity_type text  -- task | document | project
  entity_id uuid
  user_id uuid FK
  action text  -- created | updated | deleted | commented
  metadata jsonb
  created_at timestamptz

-- Custom Fields Definitions
custom_field_definitions
  id uuid PK
  project_id uuid FK
  name text
  type text  -- text | number | date | select | multi_select | user | url | checkbox
  options jsonb NULLABLE  -- for select types
  position int

-- Integrations
integrations
  id uuid PK
  workspace_id uuid FK
  provider text  -- github | slack | google | figma | linear
  config jsonb
  access_token_encrypted text
  created_at timestamptz

-- API Keys
api_keys
  id uuid PK
  workspace_id uuid FK
  name text
  key_hash text
  last_used_at timestamptz
  created_at timestamptz
  expires_at timestamptz NULLABLE

4. METADATA-DRIVEN ARCHITECTURE

This is the core architectural decision that separates FlowSpace from a plain CRUD app. Inspired directly by how Twenty CRM handles custom objects and fields — but adapted for project management.

The Problem It Solves

Standard static schemas can't let users create their own fields at runtime (e.g. "Add a 'Customer Tier' dropdown to tasks in the Sales project"). You'd normally need a schema migration for every user customisation. The metadata engine solves this without jsonb blobs or EAV anti-patterns.

How It Works: Two-Schema Architecture

Every workspace gets two PostgreSQL schemas (not tables — actual PG schemas):

PostgreSQL Instance
├── core (schema)          ← System tables: workspaces, users, members, billing
│   ├── workspaces
│   ├── users
│   ├── workspace_members
│   └── ...
│
└── workspace_{slug} (schema)   ← Dynamically created per workspace on signup
    ├── task               ← Standard object
    ├── project            ← Standard object
    ├── document           ← Standard object
    ├── _field_metadata    ← Registry of all fields (standard + custom)
    ├── _object_metadata   ← Registry of all objects (standard + custom)
    └── custom_pet_tracker ← Example custom object created by user

When a new workspace signs up, the API runs CREATE SCHEMA workspace_{slug} and then applies the standard object migrations into that schema. This gives every tenant fully isolated tables — not row-level isolation with a workspace_id column.

Metadata Tables (inside each workspace schema)

-- Object registry: every "thing" in the system
_object_metadata
  id              uuid PK DEFAULT uuidv7()
  workspace_id    uuid
  name_singular   text          -- 'task'
  name_plural     text          -- 'tasks'
  description     text
  icon            text          -- 'CheckSquare'
  is_system       boolean       -- true = standard, false = user-created
  is_active       boolean DEFAULT true
  created_at      timestamptz DEFAULT now()

-- Field registry: every field on every object
_field_metadata
  id              uuid PK DEFAULT uuidv7()
  object_id       uuid FK → _object_metadata.id
  name            text          -- 'priority'
  label           text          -- 'Priority'
  type            text          -- see FieldType enum below
  description     text
  icon            text
  is_system       boolean
  is_required     boolean DEFAULT false
  is_unique       boolean DEFAULT false
  default_value   jsonb
  options         jsonb         -- for SELECT / MULTI_SELECT: [{value, label, color}]
  settings        jsonb         -- type-specific config (e.g. min/max for NUMBER)
  position        integer       -- display order
  created_at      timestamptz DEFAULT now()

-- Relation registry: links between objects
_relation_metadata
  id              uuid PK DEFAULT uuidv7()
  from_object_id  uuid FK → _object_metadata.id
  to_object_id    uuid FK → _object_metadata.id
  type            text    -- ONE_TO_MANY | MANY_TO_MANY | ONE_TO_ONE
  from_field_name text    -- column name on from_object's table
  to_field_name   text    -- column name on to_object's table
  on_delete       text    -- CASCADE | SET_NULL | RESTRICT

FieldType Enum

enum FieldType {
  // Primitives
  TEXT        = 'TEXT',          // varchar
  RICH_TEXT   = 'RICH_TEXT',     // jsonb (BlockNote)
  NUMBER      = 'NUMBER',        // decimal
  BOOLEAN     = 'BOOLEAN',       // boolean
  DATE        = 'DATE',          // date
  DATE_TIME   = 'DATE_TIME',     // timestamptz

  // Choice fields
  SELECT      = 'SELECT',        // text with options validation
  MULTI_SELECT= 'MULTI_SELECT',  // text[] with options validation

  // Relational
  RELATION    = 'RELATION',      // FK to another object
  ARRAY       = 'ARRAY',         // jsonb array

  // Special
  URL         = 'URL',           // {label, url}
  EMAIL       = 'EMAIL',         // text + validation
  PHONE       = 'PHONE',         // text + validation
  CURRENCY    = 'CURRENCY',      // {amount_micros, currency_code}
  RATING      = 'RATING',        // integer 1-5
  POSITION    = 'POSITION',      // float8 fractional index
  UUID        = 'UUID',          // uuid
}

How Custom Fields Work at Runtime

When a user adds a custom field (e.g. "Customer Tier" select on Task):

  1. API creates the field metadata record in _field_metadata
  2. API runs DDL dynamically: ALTER TABLE workspace_acme.task ADD COLUMN customer_tier text
  3. API validates options against the metadata on every write
  4. Frontend reads _field_metadata to render the correct input type and label — no frontend deploy needed

This is exactly the pattern Twenty uses (they call it the Metadata Engine). The key difference from a jsonb blob approach: all custom fields are real PostgreSQL columns, so you get indexes, constraints, type safety, and fast queries.

Metadata API Endpoints

GET  /meta/objects              → List all object types + fields
GET  /meta/objects/:name        → Get full object schema with all fields
POST /meta/objects              → Create custom object (creates PG table)
POST /meta/objects/:name/fields → Add custom field (ALTER TABLE)
PUT  /meta/fields/:id           → Update field label/options/settings
DEL  /meta/fields/:id           → Drop column + delete metadata record

5. DATABASE MIGRATIONS

Migration Strategy: Drizzle Kit + Two-Layer System

We run two separate migration pipelines because we have two schema categories.

Layer 1 — Core Schema Migrations (standard Drizzle Kit) Static tables that never change per-workspace (users, workspaces, billing). These are normal Drizzle Kit migrations committed to source control.

packages/db/
├── schema/
│   ├── core/
│   │   ├── workspaces.ts
│   │   ├── users.ts
│   │   ├── workspace-members.ts
│   │   └── index.ts
│   └── workspace/
│       ├── tasks.ts
│       ├── projects.ts
│       ├── documents.ts
│       ├── metadata.ts     ← _object_metadata, _field_metadata tables
│       └── index.ts
├── migrations/
│   ├── core/               ← Never touched after deploy
│   │   └── 0001_init.sql
│   └── workspace/          ← Applied to every new workspace schema
│       ├── 0001_init_standard_objects.sql
│       └── 0002_add_time_tracking.sql
├── seeds/
└── drizzle.config.ts

Layer 2 — Workspace Schema Migrations (metadata-driven, runtime) When a user adds a custom field, we run ALTER TABLE directly. This is NOT a Drizzle migration — it's generated at runtime by the metadata engine and logged in a _migrations_log table inside each workspace schema for audit and rollback.

-- Logged in workspace_{slug}._migrations_log
_migrations_log
  id          uuid PK DEFAULT uuidv7()
  type        text    -- 'ADD_COLUMN' | 'DROP_COLUMN' | 'ALTER_COLUMN' | 'CREATE_TABLE'
  sql         text    -- the exact DDL that ran
  object_name text
  field_name  text
  executed_by uuid FK -- user_id who triggered it
  executed_at timestamptz DEFAULT now()
  rolled_back boolean DEFAULT false

Migration Commands (Nx targets)

# Generate migration from schema changes
pnpm nx run db:migrate:generate --name=add_time_tracking

# Apply pending migrations to all workspace schemas
pnpm nx run db:migrate:run

# Apply migrations to a specific workspace only
pnpm nx run db:migrate:run -- --workspace=acme-corp

# Rollback last migration
pnpm nx run db:migrate:rollback

# Check migration status
pnpm nx run db:migrate:status

# Reset entire database (dev only — DANGEROUS)
pnpm nx run db:migrate:reset

# Introspect existing DB → generate schema types
pnpm nx run db:migrate:introspect

drizzle.config.ts

import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  dialect: 'postgresql',
  schema: './schema/**/*.ts',
  out: './migrations',
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
  migrations: {
    table: '__drizzle_migrations',
    schema: 'core',
  },
  // Strict mode: no IF NOT EXISTS — fail loudly on conflicts
  strict: true,
  verbose: true,
});

Migration Lifecycle in CI/CD

PR opened
  └── nx affected --target=test       (unit tests against test DB)

Merge to main
  └── nx run db:migrate:run            (apply to staging DB)
  └── nx run api:e2e                   (E2E against staging)

Release tag
  └── nx run db:migrate:run            (apply to production DB)
  └── Zero-downtime: migrations are always backwards-compatible
      (add columns with DEFAULT, never drop without deprecation period)

6. DATABASE SEEDING

Three separate seed environments — each a standalone Nx target.

Seed Environments

Command Purpose Data Volume
db:seed:dev Local dev — rich, realistic data ~500 tasks, 3 workspaces, 10 users
db:seed:test CI / unit test isolation Minimal deterministic fixtures
db:seed:demo Live demo instance on flowspace.com Curated showcase data
db:seed:reset Wipe + reseed dev DB Calls migrate:reset then seed:dev

Seed Structure

packages/db/seeds/
├── data/
│   ├── users.ts          ← Deterministic users (same IDs every run)
│   ├── workspaces.ts
│   ├── projects.ts
│   ├── tasks.ts          ← ~500 tasks with realistic titles
│   ├── documents.ts      ← Sample PRD, meeting notes, wiki pages
│   ├── comments.ts
│   └── time-entries.ts
├── dev.seed.ts           ← Entry point for dev seed
├── test.seed.ts          ← Entry point for test seed
├── demo.seed.ts          ← Entry point for demo seed
└── utils/
    ├── faker.ts          ← @faker-js/faker wrappers
    └── truncate.ts       ← TRUNCATE ... CASCADE helper

Seed Implementation Pattern

// packages/db/seeds/dev.seed.ts
import { db } from '../client';
import { seedUsers } from './data/users';
import { seedWorkspaces } from './data/workspaces';
import { seedProjects } from './data/projects';
import { seedTasks } from './data/tasks';
import { truncateAll } from './utils/truncate';

async function main() {
  console.log('🌱 Seeding dev database...');

  await truncateAll(db);                         // clean slate

  const users       = await seedUsers(db);       // 10 fixed users
  const workspaces  = await seedWorkspaces(db, users);
  const projects    = await seedProjects(db, workspaces);
  const tasks       = await seedTasks(db, projects, users);

  console.log(`✅ Seeded:
    ${users.length} users
    ${workspaces.length} workspaces
    ${projects.length} projects
    ${tasks.length} tasks`);
}

main().catch(console.error).finally(() => process.exit());

Deterministic Test Fixtures

Test seeds use fixed UUIDs (not random) so assertions can reference them directly:

// packages/db/seeds/data/users.ts
export const SEED_USERS = {
  ADMIN: {
    id: '00000000-0000-0000-0000-000000000001',
    email: 'admin@test.flowspace.dev',
    name: 'Test Admin',
    role: 'owner',
  },
  MEMBER: {
    id: '00000000-0000-0000-0000-000000000002',
    email: 'member@test.flowspace.dev',
    name: 'Test Member',
    role: 'member',
  },
  GUEST: {
    id: '00000000-0000-0000-0000-000000000003',
    email: 'guest@test.flowspace.dev',
    name: 'Test Guest',
    role: 'guest',
  },
} as const;

export const SEED_PROJECTS = {
  MAIN: {
    id: '00000000-0000-0000-0000-000000000010',
    name: 'Test Project Alpha',
  },
} as const;

Seed Commands

pnpm nx run db:seed:dev       # Seed local dev DB
pnpm nx run db:seed:test      # Seed test DB (called by Vitest globalSetup)
pnpm nx run db:seed:demo      # Seed demo instance
pnpm nx run db:seed:reset     # migrate:reset + seed:dev

7. E2E TESTING

Stack: Playwright + Docker Compose

All E2E tests run against a real browser + real database — no mocks, no stubs. Every test starts from a known DB state via the test seed.

Architecture

e2e/
├── tests/
│   ├── auth/
│   │   ├── signup.spec.ts
│   │   ├── login.spec.ts
│   │   └── invite.spec.ts
│   ├── tasks/
│   │   ├── create-task.spec.ts
│   │   ├── kanban-drag.spec.ts
│   │   ├── task-detail.spec.ts
│   │   └── bulk-actions.spec.ts
│   ├── projects/
│   │   ├── create-project.spec.ts
│   │   └── views.spec.ts
│   ├── documents/
│   │   ├── create-doc.spec.ts
│   │   └── publish.spec.ts
│   ├── search/
│   │   └── global-search.spec.ts
│   ├── settings/
│   │   ├── custom-fields.spec.ts    ← Tests metadata engine
│   │   └── workspace.spec.ts
│   └── billing/
│       └── upgrade-plan.spec.ts     ← Stripe test mode
├── fixtures/
│   ├── auth.fixture.ts      ← Pre-authenticated page objects
│   ├── db.fixture.ts        ← DB client for assertion helpers
│   └── workspace.fixture.ts
├── page-objects/            ← POM pattern
│   ├── TaskPage.ts
│   ├── KanbanBoard.ts
│   ├── CommandMenu.ts
│   └── DocumentEditor.ts
├── utils/
│   ├── api-helpers.ts       ← Direct API calls to set up state faster
│   └── db-helpers.ts
└── playwright.config.ts

playwright.config.ts

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 4 : undefined,
  reporter: [
    ['html'],
    ['github'],           // inline annotations in PRs
  ],

  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },

  // Run in Chromium + Firefox + Mobile Chrome
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox',  use: { ...devices['Desktop Firefox'] } },
    { name: 'mobile',   use: { ...devices['Pixel 7'] } },
  ],

  // Start the app + seed DB before tests
  globalSetup: './utils/global-setup.ts',
  globalTeardown: './utils/global-teardown.ts',

  webServer: {
    command: 'pnpm nx run web:dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
    timeout: 120_000,
  },
});

Auth Fixture Pattern (reuse logged-in state)

// e2e/fixtures/auth.fixture.ts
import { test as base, Page } from '@playwright/test';
import { SEED_USERS } from '@flowspace/db/seeds';

type AuthFixtures = {
  adminPage: Page;
  memberPage: Page;
};

export const test = base.extend<AuthFixtures>({
  adminPage: async ({ browser }, use) => {
    // Load saved auth state — avoids full login on every test
    const ctx = await browser.newContext({
      storageState: 'e2e/.auth/admin.json',
    });
    await use(await ctx.newPage());
    await ctx.close();
  },

  memberPage: async ({ browser }, use) => {
    const ctx = await browser.newContext({
      storageState: 'e2e/.auth/member.json',
    });
    await use(await ctx.newPage());
    await ctx.close();
  },
});

export { expect } from '@playwright/test';

Page Object Model Example

// e2e/page-objects/KanbanBoard.ts
import { Page, Locator } from '@playwright/test';

export class KanbanBoard {
  readonly page: Page;
  readonly columns: Locator;

  constructor(page: Page) {
    this.page = page;
    this.columns = page.locator('[data-testid="kanban-column"]');
  }

  column(name: string) {
    return this.page.locator(`[data-testid="kanban-column"][data-name="${name}"]`);
  }

  card(title: string) {
    return this.page.locator(`[data-testid="kanban-card"][data-title="${title}"]`);
  }

  async dragCardToColumn(cardTitle: string, targetColumn: string) {
    const card   = this.card(cardTitle);
    const target = this.column(targetColumn);
    await card.dragTo(target);
  }

  async createCard(column: string, title: string) {
    await this.column(column).locator('[data-testid="add-card"]').click();
    await this.page.keyboard.type(title);
    await this.page.keyboard.press('Enter');
  }
}

E2E Test Example

// e2e/tests/tasks/kanban-drag.spec.ts
import { test, expect } from '../../fixtures/auth.fixture';
import { KanbanBoard } from '../../page-objects/KanbanBoard';

test.describe('Kanban board', () => {
  test.beforeEach(async ({ adminPage }) => {
    await adminPage.goto('/acme-corp/engineering/backend-api');
  });

  test('drag task from Todo to In Progress', async ({ adminPage }) => {
    const board = new KanbanBoard(adminPage);

    await expect(board.column('Todo')).toBeVisible();
    await board.dragCardToColumn('Fix auth bug', 'In Progress');

    // Assert DB state directly via API helper
    await expect(adminPage.locator('[data-testid="toast-success"]'))
      .toContainText('Task updated');
    await expect(board.column('In Progress').locator('text=Fix auth bug'))
      .toBeVisible();
  });

  test('create a new card inline', async ({ adminPage }) => {
    const board = new KanbanBoard(adminPage);
    await board.createCard('Todo', 'New E2E test task');

    await expect(board.card('New E2E test task')).toBeVisible();
  });
});

E2E in CI (Nx target + GitHub Actions)

# .github/workflows/e2e.yml
- name: Run E2E tests
  run: |
    docker compose -f docker-compose.test.yml up -d
    pnpm nx run db:migrate:run
    pnpm nx run db:seed:test
    pnpm nx run e2e:test --ci
    docker compose -f docker-compose.test.yml down

- name: Upload Playwright report
  uses: actions/upload-artifact@v4
  if: always()
  with:
    name: playwright-report
    path: e2e/playwright-report/

E2E Coverage Goals

Area Critical Paths Covered
Auth Signup, magic link, Google OAuth, invite flow
Tasks Create, update, delete, drag-drop, bulk actions, custom fields
Kanban All views, column reorder, WIP limits
Documents Create, edit with BlockNote, publish to web, search
Metadata Add custom field → appears in UI → persists after reload
Search Task search, doc search, cross-entity results
Settings Invite member, change role, generate API key
Billing Upgrade to Pro (Stripe test mode), feature gate enforcement

8. FEATURE SPECIFICATIONS

8.1 AUTHENTICATION & ONBOARDING

Routes: /login /signup /invite/[token] /onboarding

Features:

  • Email + password signup
  • Magic link (passwordless)
  • Google OAuth, GitHub OAuth
  • Workspace invite links (expirable, role-scoped)
  • Onboarding wizard: create workspace → invite team → create first project → choose template
  • Profile settings: name, avatar, timezone, notification preferences

Security:

  • JWT access tokens (15min) + refresh tokens (30d) in httpOnly cookies
  • PKCE for OAuth flows
  • Rate limiting on auth endpoints (Upstash Redis)
  • Email verification required before workspace creation

8.2 WORKSPACE & SPACE MANAGEMENT

Routes: /[workspace-slug]/ /[workspace-slug]/settings

Workspace:

  • Unique slug-based URLs (e.g. app.flowspace.com/acme-corp)
  • Custom logo + branding
  • Plan management
  • Danger zone: export all data, delete workspace

Spaces:

  • Color + icon per space
  • Private spaces (invite-only)
  • Space-level member management
  • Space templates (Engineering, Marketing, Design, etc.)

8.3 PROJECT MANAGEMENT

Routes: /[workspace]/[space]/[project]/

Views (all projects support all views)

1. List View

  • Grouped by status by default
  • Collapsible groups
  • Inline task creation in any group
  • Sort by: due date, priority, assignee, created date, custom fields
  • Column visibility toggle
  • Bulk select + bulk actions (assign, change status, delete)
  • Custom field columns visible inline

2. Kanban Board View

  • Drag tasks between status columns
  • Drag to reorder within column (fractional indexing)
  • Collapsible columns
  • Column WIP limits (optional)
  • Swimlanes: group by assignee, priority, or custom field
  • Quick add card at bottom of each column
  • Card previews: priority badge, assignee avatar, due date, subtask count

3. Timeline / Gantt View

  • Date-based horizontal bars per task
  • Drag to reschedule, drag edges to resize duration
  • Group by: status, assignee, project
  • Dependency arrows (task A blocks task B)
  • Today line indicator
  • Zoom: day / week / month / quarter

4. Calendar View

  • Monthly / weekly grid
  • Tasks plotted by due date
  • Drag to reschedule
  • Multi-day tasks span across days
  • Quick create by clicking a date

5. Table View

  • Spreadsheet-style with all fields as columns
  • Add custom field columns
  • Sort, filter, group by any column
  • Inline edit any cell
  • Row height: compact / comfortable / spacious
  • Freeze first column

6. Dashboard View (per project)

  • Drag-and-drop widget grid (react-grid-layout)
  • Widgets: task count by status (donut), burndown chart, team workload (bar), overdue tasks list, recent activity feed, time tracked this week
  • Saveable dashboard layouts

8.4 TASK DETAIL

Routes: Right panel slide-over OR /[workspace]/task/[taskId] (full page)

Core fields:

  • Title (inline edit)
  • Status (dropdown)
  • Priority (urgent / high / medium / low / none)
  • Assignee (single or multiple)
  • Due date + start date (date picker)
  • Tags (autocomplete, create new)
  • Time estimate
  • Parent task (breadcrumb for subtasks)

Rich description:

  • BlockNote editor (slash commands)
  • Blocks: paragraph, heading 1-3, bullet list, numbered list, checklist, quote, callout, divider, image, video embed, code block, table, toggle, mention (@user, #task, [[doc]])
  • Drag blocks to reorder
  • Paste URLs → auto-embed (Figma, Loom, YouTube, GitHub PR)

Subtasks:

  • Unlimited nesting (max 5 levels recommended)
  • Progress bar on parent showing subtask completion %
  • Convert subtask to top-level task

Comments:

  • Rich text comments (same block editor, simpler palette)
  • Threaded replies
  • Emoji reactions
  • @mentions with notification
  • Edit + delete own comments
  • Resolve/unresolve comment threads

Attachments:

  • Drag-and-drop file upload
  • Image preview inline
  • S3 storage with signed URLs
  • Max 25MB per file (free) / 250MB (pro)

Activity Log:

  • Every field change logged: who, what, when
  • Chronological timeline on task

Custom Fields:

  • Text, Number, Date, Select, Multi-select, User, URL, Checkbox, Formula (Phase 2)
  • Defined per project, visible on task detail + table view

Time Tracking:

  • Start/stop timer on any task
  • Manual time entry
  • Per-task time log
  • Workspace-wide time reports

8.5 DOCUMENTS (Notion-like)

Routes: /[workspace]/docs/ /[workspace]/docs/[docId]

Features:

  • Nested page tree (sidebar)
  • Drag to reorder / nest
  • BlockNote full editor (same as task description, extended with full-width blocks)
  • Page icon (emoji or custom) + cover image
  • Breadcrumb navigation
  • Link to tasks inline: /task TASK-123
  • Backlinks panel (which docs link to this doc)
  • Full-text search (Meilisearch)
  • Version history (last 100 versions, pro: unlimited)
  • Publish to web: toggle → get shareable read-only URL
  • Export: Markdown, PDF
  • Import: Markdown, Notion export (JSON zip)
  • Templates: meeting notes, PRD, retrospective, weekly update

8.6 TEAM COLLABORATION

Realtime Presence:

  • Avatars showing who is viewing same task/doc
  • Live cursor positions in docs (Liveblocks)
  • Typing indicators in comments

Notifications:

  • In-app notification center (bell icon)
  • Email digests (immediate / hourly / daily — user preference)
  • Notification types: @mention, assigned to task, task due tomorrow, comment reply, task status changed, doc shared with you
  • Mark all as read, archive

Team Inbox:

  • All items assigned to me
  • Filtered views: overdue, due today, due this week, recently updated

@Mentions:

  • Works across tasks, comments, docs
  • Creates notification to mentioned user
  • Highlights mention in rendered content

8.7 SEARCH

Global Search (Cmd/Ctrl + K):

  • Fuzzy search across tasks, docs, projects, comments
  • Powered by Meilisearch
  • Filters: type, project, assignee, date range, status
  • Recent searches
  • Keyboard navigation
  • Results grouped by type with icons

8.8 CUSTOM WORKFLOWS

Automations (Phase 2):

  • Trigger → Condition → Action model
  • Triggers: task created, status changed, due date approaches, custom field changed, comment added
  • Actions: assign user, change status, send notification, create subtask, post to Slack, move to project, set custom field
  • Visual automation builder (no-code)
  • Pre-built automation templates

8.9 INTEGRATIONS

Phase 1 (MVP):

  • Slack: post activity, task create from slash command
  • GitHub: link PRs to tasks, auto-close tasks on merge
  • Google Calendar: sync due dates

Phase 2:

  • Figma: embed designs in tasks/docs
  • Linear: bi-directional sync
  • Zapier / Make webhooks
  • Google Drive: attach files
  • Loom: embed videos

Developer API:

  • REST API (documented with Scalar)
  • API key management in workspace settings
  • Webhooks for all entity events
  • Rate limits: 1000 req/min (pro), 100 req/min (free)
  • SDK: official TypeScript client (Phase 2)

8.10 SETTINGS

Workspace Settings:

  • General: name, slug, logo, timezone
  • Members: invite, role management, remove
  • Billing: plan, payment method, invoices
  • Integrations
  • API Keys
  • Import / Export
  • Danger zone

Personal Settings:

  • Profile: name, avatar, email
  • Notifications preferences
  • Appearance: light / dark / system
  • Language / timezone
  • Connected accounts (Google, GitHub)
  • Active sessions

9. UI/UX SPECIFICATIONS

Design System

Color Tokens:

/* Brand */
--brand-primary: #5B4CF5      /* violet */
--brand-secondary: #8B7BF8

/* Neutral (based on Twenty's palette approach) */
--gray-50 through --gray-950

/* Semantic */
--status-todo: #94A3B8
--status-in-progress: #6366F1
--status-done: #22C55E
--status-cancelled: #EF4444

/* Priority */
--priority-urgent: #EF4444
--priority-high: #F97316
--priority-medium: #F59E0B
--priority-low: #6366F1
--priority-none: #94A3B8

Typography:

  • Font: Geist Sans (same as Vercel/Linear aesthetic)
  • Mono: Geist Mono
  • Scale: 12 / 13 / 14 / 16 / 18 / 20 / 24 / 32 / 40px

Layout:

  • Left sidebar: 240px (collapsible to 48px icon rail)
  • Right panel (task detail): 480px slide-over
  • Main content: fluid remaining width
  • Top bar: 48px fixed

Sidebar Structure:

[Workspace Logo + Name ▾]
  ─────────────────────
  ⌕ Search        Cmd+K
  ⌂ Home
  📥 My Tasks
  🔔 Notifications
  ─────────────────────
  SPACES
  ▾ Engineering
    ├ 📋 Backend API
    ├ 📋 Frontend
    └ + New Project
  ▾ Marketing
    └ 📋 Q1 Campaign
  + New Space
  ─────────────────────
  📄 Docs
  ─────────────────────
  ⚙ Settings
  [Avatar] [Name]

Design Principles:

  1. Density matters — compact default, spacious optional. Power users live in list view.
  2. Keyboard first — every action has a shortcut. Modal for shortcut reference (?)
  3. Optimistic UI — all mutations update immediately, rollback on error. Never block user.
  4. No dead ends — empty states have clear CTAs. First-time UX is guided.
  5. Progressive disclosure — basic fields visible, advanced fields behind "More options"

Keyboard Shortcuts:

Cmd+K           Global search
Cmd+N           New task
Cmd+/           Toggle sidebar
C               Create task (when in project view)
G then H        Go to Home
G then M        Go to My Tasks
Cmd+Enter       Submit (forms, comments)
Escape          Close modals/panels
1-4             Switch views (List/Board/Timeline/Calendar)
?               Open shortcuts modal

10. API DESIGN

Structure

All API calls go through tRPC routers for type safety. Also exposes REST for external integrations.

Base URL: https://api.flowspace.com/v1

Core Routers (tRPC):

// Tasks
tasks.create
tasks.update
tasks.delete
tasks.list
tasks.get
tasks.addComment
tasks.listComments
tasks.startTimer
tasks.stopTimer

// Projects
projects.create
projects.update
projects.delete
projects.list

// Docs
docs.create
docs.update
docs.delete
docs.list
docs.publish

// Workspaces
workspaces.create
workspaces.update
workspaces.listMembers
workspaces.inviteMember
workspaces.removeMember

// Users
users.me
users.update
users.notifications

REST Endpoints (external API):

GET    /v1/tasks
POST   /v1/tasks
GET    /v1/tasks/:id
PATCH  /v1/tasks/:id
DELETE /v1/tasks/:id

GET    /v1/projects
POST   /v1/projects
GET    /v1/projects/:id

GET    /v1/docs
POST   /v1/docs

POST   /v1/webhooks
DELETE /v1/webhooks/:id

Webhook Payload:

{
  "event": "task.status_changed",
  "timestamp": "2025-01-01T00:00:00Z",
  "workspace_id": "uuid",
  "data": {
    "task_id": "uuid",
    "previous_status": "todo",
    "new_status": "in_progress",
    "changed_by": "user_id"
  }
}

11. SAAS PRICING MODEL

Plans

Feature Free Pro Business Enterprise
Members 5 Unlimited Unlimited Unlimited
Spaces 3 Unlimited Unlimited Unlimited
Projects per space 5 Unlimited Unlimited Unlimited
Storage 1GB 50GB 250GB Custom
File size limit 5MB 25MB 100MB Custom
Doc version history 30 days Unlimited Unlimited Unlimited
API calls 100/min 1000/min 5000/min Custom
Automations 5 100 Unlimited Unlimited
Custom fields 5 50 Unlimited Unlimited
Integrations 2 All All + Priority Custom
Guest access
SSO / SAML
Audit logs 30 days 1 year Unlimited
SLA 99.9% 99.99%
Price Free $12/user/mo $20/user/mo Custom

Billing via Stripe:

  • Stripe Checkout for subscription
  • Stripe Customer Portal for self-service plan changes
  • Annual billing = 2 months free (17% discount)
  • Failed payment grace period: 7 days → downgrade to free

12. DEVELOPMENT PHASES

Phase 1 — MVP (Months 1–4)

Goal: Ship a usable product. Public open-source repo. Basic cloud SaaS.

  • Monorepo setup (Nx + pnpm)
  • Auth (email/password + Google OAuth)
  • Workspace + Space + Project CRUD
  • Task CRUD (title, status, assignee, due date, priority)
  • List View + Kanban Board View
  • Task detail panel (description with BlockNote, comments)
  • Basic notifications (in-app)
  • Documents (basic page creation + nested pages)
  • Global search (Meilisearch)
  • Docker Compose for self-hosting
  • Deployment: Vercel + Railway
  • Landing page (flowspace.com)
  • Basic docs site

Open Source Goals:

  • Public GitHub repo from day 1
  • MIT license on core (AGPL for cloud-specific features)
  • CONTRIBUTING.md, good first issues tagged
  • GitHub Discussions for community

Phase 2 — Growth (Months 5–9)

Goal: Feature parity with ClickUp mid-tier. Paying customers.

  • Timeline / Gantt view
  • Calendar view
  • Table view with custom columns
  • Custom fields (all types)
  • Time tracking
  • Automations builder
  • Dashboard widgets
  • Slack + GitHub integrations
  • Version history (docs)
  • Subtasks (multi-level)
  • Bulk actions
  • Import from Notion / ClickUp / CSV
  • REST API + API key management
  • Webhooks
  • Stripe billing integration
  • Email notifications (Resend)
  • Mobile-responsive web

Phase 3 — Scale (Months 10–18)

Goal: Enterprise-ready. Desktop app. Ecosystem.

  • SSO / SAML
  • Audit logs
  • Advanced permissions (field-level)
  • Figma + Loom integrations
  • Zapier / Make native integration
  • TypeScript SDK (npm package)
  • Electron desktop app
  • White-label for Enterprise
  • AI features: task summarization, auto-assign, smart due dates (Claude API)
  • Analytics dashboards
  • Portfolio view (cross-project reporting)
  • Resource management (team capacity)
  • Forms (public intake → task creation)

13. OPEN SOURCE STRATEGY

Repository Structure

  • apps/web — MIT licensed
  • apps/api — MIT licensed (core)
  • apps/cloud — Proprietary (SaaS-specific features: billing, SSO, audit logs)

This is the Open Core model used by GitLab, Metabase, Plane.so.

Community Flywheel

  1. Launch on GitHub → ProductHunt → Hacker News
  2. Good docs + one-command self-host (docker compose up)
  3. First 100 GitHub stars → write blog post on dev.to
  4. Community features: integrations contributed by OSS contributors
  5. Convert top OSS users → cloud SaaS (they're already users, just on their own server)

Self-Host Experience

# One command to self-host
git clone https://github.com/flowspace/flowspace
cd flowspace
cp .env.example .env
docker compose up -d

# App live at http://localhost:3000

14. PERFORMANCE TARGETS

Metric Target
Page load (LCP) < 1.5s
Task open (interaction) < 200ms
Search results < 100ms
Kanban drag response < 16ms (60fps)
API P99 latency < 300ms
Uptime (SaaS) 99.9%
Time to first task < 5 min (onboarding)

Optimizations:

  • React Server Components for initial renders
  • Optimistic mutations (Zustand + TanStack Query)
  • Virtualized lists (TanStack Virtual) for 10,000+ tasks
  • Debounced search (300ms)
  • Image optimization via Next.js Image
  • Fractional indexing for drag-and-drop order (no renumbering)
  • Database indexes on all FK columns + frequently filtered columns
  • Redis cache for workspace member lists, project metadata

15. SECURITY REQUIREMENTS

  • All user data encrypted at rest (AES-256)
  • TLS 1.3 in transit
  • Workspace data isolation at DB level (workspace_id on every query)
  • OWASP Top 10 compliance
  • Rate limiting on all public endpoints
  • File uploads: virus scan (ClamAV), type validation, SSRF protection
  • Content Security Policy headers
  • No third-party analytics that share user data
  • GDPR compliance: data export, right to deletion
  • SOC2 Type II (target: Month 18)

16. TESTING STRATEGY

Unit Tests (Vitest)

  • All utility functions
  • tRPC router logic
  • Drizzle query builders

Integration Tests (Vitest + Supertest)

  • API endpoints with test DB
  • Auth flows
  • File upload/download

E2E Tests (Playwright)

  • Critical paths: signup → create workspace → create task → assign → comment
  • Kanban drag and drop
  • Document creation + publish
  • Payment flow (Stripe test mode)

Performance Tests

  • k6 load testing for API endpoints
  • Lighthouse CI on every PR for frontend

17. METRICS TO TRACK (PostHog)

Activation: % of signups who create first task within 24h Engagement: DAU/MAU ratio Retention: Week 1, Week 4, Month 3 retention curves Revenue: MRR, ARR, churn rate, LTV Product: Most used views, task completion rates, doc creation rate Self-host: Docker pulls, GitHub stars (proxy for OSS health)


18. COMPETITIVE POSITIONING

Product Price Open Source Best At
FlowSpace Free–$20/user ✅ Core Balance of power + simplicity
ClickUp Free–$19/user Feature breadth
Notion Free–$16/user Docs + flexibility
Linear Free–$18/user Engineering teams
Plane.so Free–$6/user ClickUp OSS clone
Jira $0–$16/user Enterprise / complex workflows

FlowSpace wins on:

  1. Open source (no vendor lock-in) vs ClickUp/Notion/Linear
  2. Better UX than Jira (much simpler)
  3. More structured than Notion (actual project management)
  4. More powerful than Plane.so (docs + automations + custom fields)
  5. Better design + DX than traditional OSS tools

19. LAUNCH CHECKLIST

Pre-Launch

  • Landing page live (waitlist)
  • GitHub repo public (README, good first issues)
  • Self-host Docker Compose working
  • Core MVP features complete
  • Stripe integration tested
  • Error monitoring (Sentry) active
  • Analytics (PostHog) active
  • GDPR privacy policy + ToS

Launch Day

  • ProductHunt submission prepared (screenshots, GIF demo)
  • Hacker News "Show HN" post written
  • Twitter/X thread written
  • Dev.to article: "Why I built an open-source ClickUp alternative"
  • YouTube demo video (5 min)

Post-Launch Week 1

  • Respond to every GitHub issue within 24h
  • Discord server for community
  • Blog post: "We hit 500 GitHub stars"
  • Collect user interviews (10 users, 30 min each)

Document version: 1.0 | Written as senior developer specification | All features subject to user validation before build