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
35 changes: 35 additions & 0 deletions .github/workflows/habit-tracker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Habit Tracker checks

on:
pull_request:
paths:
- 'applications/habit-tracker/**'
- '.github/workflows/habit-tracker.yml'
push:
branches: [main]
paths:
- 'applications/habit-tracker/**'
- '.github/workflows/habit-tracker.yml'
workflow_dispatch:

permissions:
contents: read

jobs:
check:
runs-on: ubuntu-latest
defaults:
run:
working-directory: applications/habit-tracker
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.22.1
cache: npm
cache-dependency-path: applications/habit-tracker/package-lock.json
- run: npm ci --ignore-scripts
- run: npm test
- run: npm run build

# Integration tests need a Postgres database; see the README.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ ClickHouse ships official client libraries for [C# / .NET](https://clickhouse.co
- [Shortwave link shortener](./applications/shortwave/README.md): Build and deploy a link shortener using ClickHouse Managed Postgres for application data, ClickPipes for metadata sync, and ClickHouse for click analytics.
- [Feature Request Board](./applications/feature-request-board/README.md): Build a Next.js and Prisma board with Clerk sign-in, request ownership, and one vote per user on ClickHouse Managed Postgres.
- [Workshop Booking API](./applications/workshop-booking/README.md): Build a FastAPI and SQLAlchemy application on ClickHouse Managed Postgres with transactional seat allocation, cancellation, and safe retries.
- [Habit Tracker](./applications/habit-tracker/README.md): Build a Next.js and Drizzle ORM habit tracker on ClickHouse Managed Postgres with one check-in per habit per day and streaks computed in SQL.
- [Postgres-to-ClickHouse data modeling](./postgresql-clickhouse-data-modeling/README.md): Replicate a tiny fixture from PostgreSQL to ClickHouse with PeerDB, then verify inserts, updates, and deletes. Follow the separate ClickHouse Managed Postgres and ClickPipes walkthrough for ClickHouse Cloud; a larger Stack Overflow import is optional.

### ClickStack: logs, metrics, traces, and session replay
Expand Down
8 changes: 8 additions & 0 deletions applications/habit-tracker/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Runtime connection as the least-privilege app role. No query parameters:
# TLS is configured from PGSSLROOTCERT in src/db.ts.
DATABASE_URL=postgres://habit_tracker_app:REPLACE_ME@REPLACE_HOST:5432/postgres
# CA certificate from `clickhousectl cloud postgres certs get`. Leave unset for
# local Postgres without TLS.
PGSSLROOTCERT=/absolute/path/to/.deployment/ca.pem
# "Today" is computed in this IANA time zone. Defaults to UTC.
APP_TIME_ZONE=UTC
9 changes: 9 additions & 0 deletions applications/habit-tracker/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules/
.next/
next-env.d.ts
*.tsbuildinfo
.env*
!.env.example
.deployment/
.clickhouse/
.DS_Store
326 changes: 326 additions & 0 deletions applications/habit-tracker/README.md

Large diffs are not rendered by default.

Binary file added applications/habit-tracker/docs/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions applications/habit-tracker/drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { defineConfig } from "drizzle-kit";
import { tlsOptions } from "./src/db";

// drizzle-kit opens its own connection, so it needs the same TLS settings as src/db.ts.
const url = new URL(
process.env.DATABASE_URL ?? "postgres://localhost:5432/postgres",
);

export default defineConfig({
dialect: "postgresql",
schema: "./src/schema.ts",
out: "./drizzle",
schemaFilter: ["habit_tracker"],
dbCredentials: {
host: url.hostname,
port: Number(url.port || 5432),
user: decodeURIComponent(url.username),
password: decodeURIComponent(url.password),
database: url.pathname.slice(1) || "postgres",
ssl: tlsOptions(url.href, process.env.PGSSLROOTCERT),
},
});
26 changes: 26 additions & 0 deletions applications/habit-tracker/drizzle/0000_init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
CREATE SCHEMA "habit_tracker";
--> statement-breakpoint
CREATE TABLE "habit_tracker"."check_ins" (
"habit_id" integer NOT NULL,
"day" date NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "check_ins_habit_id_day_pk" PRIMARY KEY("habit_id","day")
);
--> statement-breakpoint
CREATE TABLE "habit_tracker"."habits" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "habit_tracker"."habits_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"user_id" integer NOT NULL,
"name" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"archived_at" timestamp with time zone,
CONSTRAINT "habits_name_length" CHECK (char_length("habit_tracker"."habits"."name") between 1 and 60)
);
--> statement-breakpoint
CREATE TABLE "habit_tracker"."users" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "habit_tracker"."users_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"name" text NOT NULL
);
--> statement-breakpoint
ALTER TABLE "habit_tracker"."check_ins" ADD CONSTRAINT "check_ins_habit_id_habits_id_fk" FOREIGN KEY ("habit_id") REFERENCES "habit_tracker"."habits"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "habit_tracker"."habits" ADD CONSTRAINT "habits_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "habit_tracker"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "habits_active_by_user" ON "habit_tracker"."habits" USING btree ("user_id") WHERE "habit_tracker"."habits"."archived_at" is null;
204 changes: 204 additions & 0 deletions applications/habit-tracker/drizzle/meta/0000_snapshot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
{
"id": "47862eeb-58a6-48bd-8151-6b46655f4082",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"habit_tracker.check_ins": {
"name": "check_ins",
"schema": "habit_tracker",
"columns": {
"habit_id": {
"name": "habit_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"day": {
"name": "day",
"type": "date",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"check_ins_habit_id_habits_id_fk": {
"name": "check_ins_habit_id_habits_id_fk",
"tableFrom": "check_ins",
"tableTo": "habits",
"schemaTo": "habit_tracker",
"columnsFrom": [
"habit_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"check_ins_habit_id_day_pk": {
"name": "check_ins_habit_id_day_pk",
"columns": [
"habit_id",
"day"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"habit_tracker.habits": {
"name": "habits",
"schema": "habit_tracker",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "habits_id_seq",
"schema": "habit_tracker",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"archived_at": {
"name": "archived_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"habits_active_by_user": {
"name": "habits_active_by_user",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"where": "\"habit_tracker\".\"habits\".\"archived_at\" is null",
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"habits_user_id_users_id_fk": {
"name": "habits_user_id_users_id_fk",
"tableFrom": "habits",
"tableTo": "users",
"schemaTo": "habit_tracker",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {
"habits_name_length": {
"name": "habits_name_length",
"value": "char_length(\"habit_tracker\".\"habits\".\"name\") between 1 and 60"
}
},
"isRLSEnabled": false
},
"habit_tracker.users": {
"name": "users",
"schema": "habit_tracker",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "users_id_seq",
"schema": "habit_tracker",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {
"habit_tracker": "habit_tracker"
},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
13 changes: 13 additions & 0 deletions applications/habit-tracker/drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1792587155318,
"tag": "0000_init",
"breakpoints": true
}
]
}
7 changes: 7 additions & 0 deletions applications/habit-tracker/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { NextConfig } from "next";

const config: NextConfig = {
poweredByHeader: false,
};

export default config;
Loading
Loading