Add Hono backend with PostgreSQL, auth, cloud saves, and leaderboard
Server app (apps/server) with Hono framework and Drizzle ORM: - PostgreSQL schema: users, saves, leaderboard, achievements tables - Anonymous auth with UUID tokens, optional email/password linking - Cloud save API: list, get, upsert, delete with auto-save hook - Leaderboard API: per-category rankings with score submission - CORS configured for dev server ports - Typed middleware with Hono env variables Frontend cloud save integration: - API client with auth token management in localStorage - useCloudSave hook auto-saves every 300 ticks when authenticated - Vite env type declarations for VITE_API_URL Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { pgTable, uuid, text, timestamp, jsonb, integer, boolean, index } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const users = pgTable('users', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
anonToken: uuid('anon_token').defaultRandom().notNull().unique(),
|
||||
email: text('email').unique(),
|
||||
passwordHash: text('password_hash'),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
lastSeenAt: timestamp('last_seen_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const saves = pgTable('saves', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
userId: uuid('user_id').notNull().references(() => users.id),
|
||||
companyName: text('company_name').notNull(),
|
||||
saveVersion: integer('save_version').notNull(),
|
||||
gameData: jsonb('game_data').notNull(),
|
||||
tickCount: integer('tick_count').notNull().default(0),
|
||||
era: text('era').notNull().default('startup'),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
}, (table) => [
|
||||
index('saves_user_id_idx').on(table.userId),
|
||||
]);
|
||||
|
||||
export const leaderboard = pgTable('leaderboard', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
userId: uuid('user_id').notNull().references(() => users.id),
|
||||
companyName: text('company_name').notNull(),
|
||||
category: text('category').notNull(),
|
||||
score: integer('score').notNull(),
|
||||
era: text('era').notNull(),
|
||||
tickCount: integer('tick_count').notNull(),
|
||||
submittedAt: timestamp('submitted_at').defaultNow().notNull(),
|
||||
}, (table) => [
|
||||
index('leaderboard_category_score_idx').on(table.category, table.score),
|
||||
]);
|
||||
|
||||
export const achievements = pgTable('achievements', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
userId: uuid('user_id').notNull().references(() => users.id),
|
||||
achievementId: text('achievement_id').notNull(),
|
||||
unlockedAt: timestamp('unlocked_at').defaultNow().notNull(),
|
||||
}, (table) => [
|
||||
index('achievements_user_id_idx').on(table.userId),
|
||||
]);
|
||||
Reference in New Issue
Block a user