Noodle: Open-Source Student Productivity Platform

July 5, 2023

|repo-review

by Florian Narr

Noodle: Open-Source Student Productivity Platform

Noodle is a student productivity platform in active development — the goal is one place for notes, flashcards, task management, grade tracking, and calendar. Think Notion meets Anki but purpose-built for university life.

Why I starred it

The premise is familiar — "why are students using five different apps?" — but what made me stop on this one was the stack. This isn't a side project with a raw SQLite file and some client-side state. It's a full production-grade setup: tRPC v11, Drizzle ORM with Neon Postgres, Clerk for auth, Upstash Redis, and Resend for transactional email. For a pre-MVP app, the engineering foundations are unusually solid.

12k stars for something explicitly marked as pre-MVP also suggests the idea hit a nerve.

How it works

The app is a Next.js 14 project under src/, split into (site) (landing, auth) and (dashboard)/app (the actual product). Route protection is handled in src/middleware.ts with Clerk:

const isProtectedRoute = createRouteMatcher(['/app(.*)']);

export default clerkMiddleware((auth, req) => {
  if (isProtectedRoute(req)) {
    auth().protect();
  }
});

Clean. Any route under /app requires auth, everything else is public.

The data layer is in src/db/index.ts. They're using Drizzle over Neon's serverless HTTP driver — not WebSockets, just HTTP:

const sql = neon(env.DATABASE_URL);
export const db = drizzle(sql, { schema, logger: env.NODE_ENV === 'development' });

This is the right call for a Next.js app on serverless infrastructure. Neon's HTTP driver avoids connection pool exhaustion that bites you when you spin up 50 concurrent lambda invocations.

The schema lives in src/db/schema/modules.ts and uses drizzle-zod to derive Zod validators directly from the table definition:

export const modulesTable = pgTable('modules', {
  id: uuid('id').primaryKey().unique().defaultRandom().notNull(),
  user_id: text('user_id').notNull(),
  name: text('name').notNull(),
  icon: text('icon').default('default').notNull(),
  color: text('color').default('default').notNull(),
  archived: boolean('archived').default(false).notNull(),
  credits: integer('credits').default(0).notNull(),
  ...
});

export const insertModuleSchema = createInsertSchema(modulesTable).extend({ ... });

The tRPC context in src/server/trpc.ts injects db, resend, and redis into every procedure. The protectedProcedure middleware calls currentUser() from Clerk and throws UNAUTHORIZED if it's missing — one place to enforce auth across all routers.

The most interesting file in the whole repo is the early access email validator in src/server/routers/early-access.ts. They wrote a heuristic spam email scorer from scratch instead of reaching for a library:

function scoreEmail(email: string): number {
  let score = 0;
  // penalizes known automated domains (@slack.com, @github.com, @hubspot.com...)
  // penalizes usernames with bot keywords: 'noreply', 'admin', 'alerts'...
  // penalizes high number density in username
  // penalizes gibberish via vowel/consonant ratio
  // penalizes alternating letters-and-numbers patterns
  return score;
}

function isLikelyHuman(email: string, threshold = 30): boolean {
  return scoreEmail(email) < threshold;
}

isEmailGibberish checks the vowel-to-consonant ratio: if a username has zero vowels, or more than 8 consonants with fewer than 2 vowels, it scores as gibberish. It's not foolproof but it's genuinely thoughtful — better than a regex blocklist alone, cheaper than an external service.

The environment config uses @t3-oss/env-nextjs with Zod schemas for each variable. Type-safe env access at build time, validated at startup. If DATABASE_URL is missing, you get a clear error before the app boots.

Using it

Self-hosting requires Neon, Clerk, Upstash, and Resend accounts. Migrations run via Drizzle Kit:

bun run db:generate
bun run db:migrate
bun run dev

There's no Docker setup, so you're on your own for local orchestration of the external services.

Rough edges

The README is honest: the preview screenshot is a UI mockup, not the current product. The MVP features are limited to notes and flashcards. The flashcard generation from notes (described as AI-powered) isn't implemented yet.

There are no tests. Zero. The @happy-dom/global-registrator dev dependency is there, suggesting they planned for it, but the test directory doesn't exist.

The drizzle/0000_funny_johnny_blaze.sql migration only creates two tables: early_access and modules. That's the entire data model so far — most of the platform's planned features don't have schemas yet.

The README also hasn't been updated to reflect the switch from SQLite/Turso (listed in repo topics) to Neon Postgres. The code is Neon. The topics say Turso/SQLite. Minor but worth knowing if you're reading the docs.

Bottom line

Not usable as a student tool today — it's genuinely pre-MVP. Worth watching if you're interested in how a well-structured Next.js + tRPC + Drizzle app is organized, because the engineering groundwork is better than most projects twice its maturity. The email scoring code alone is worth five minutes of your time.

noodle-run/noodle on GitHub
noodle-run/noodle