diff --git a/docs/queues/AnalyticsAggregation.md b/docs/queues/AnalyticsAggregation.md
new file mode 100644
index 00000000..4a394dc1
--- /dev/null
+++ b/docs/queues/AnalyticsAggregation.md
@@ -0,0 +1,11 @@
+# AnalyticsAggregation BullMQ Queue
+
+The `AnalyticsAggregation` queue is responsible for compiling and aggregating raw telemetry and event data into materialized views or time-series rollup tables.
+
+## Architecture
+
+1. **Producer**: A cron-based job triggers the aggregation every hour.
+2. **Worker**: The worker queries the raw event logs, aggregates counts and unique user metrics by time bucket (hourly, daily, weekly), and `UPSERT`s them into the `analytics_rollups` table.
+
+## Retention Policy
+The raw event logs are kept for 7 days, after which they are pruned by the `DataCleanup` job. The aggregated data is kept indefinitely.
diff --git a/docs/queues/LocationHistorySync.md b/docs/queues/LocationHistorySync.md
new file mode 100644
index 00000000..347af0f2
--- /dev/null
+++ b/docs/queues/LocationHistorySync.md
@@ -0,0 +1,12 @@
+# LocationHistorySync BullMQ Queue
+
+The `LocationHistorySync` queue is responsible for flushing user location history from the Redis fast-path into the PostgreSQL database.
+
+## Architecture
+
+1. **Redis Cache**: High-frequency location updates are written to Redis sorted sets (ZSET) by the fast-path WebSocket handlers.
+2. **Cron Producer**: A repeating BullMQ job runs every 5 minutes to trigger the sync process.
+3. **Worker**: The worker pops the data from Redis, batches it into bulk `INSERT` statements using Drizzle ORM, and stores it in PostGIS.
+
+## Fault Tolerance
+If the worker fails, the data remains in Redis and will be picked up on the next cron execution, ensuring zero data loss.
diff --git a/docs/queues/NotificationQueue.md b/docs/queues/NotificationQueue.md
new file mode 100644
index 00000000..3f12bf73
--- /dev/null
+++ b/docs/queues/NotificationQueue.md
@@ -0,0 +1,24 @@
+# NotificationQueue Configuration
+
+The `NotificationQueue` handles sending push notifications and emails to users asynchronously via BullMQ.
+
+## Architecture
+
+1. **Redis**: Used as the backing store for BullMQ.
+2. **Worker**: A dedicated worker processes the `NotificationQueue` and interfaces with APNs/FCM or SendGrid.
+3. **Producer**: The main Fastify server adds jobs to the queue when specific events occur (e.g. `MESSAGE_RECEIVED`, `ROOM_INVITE`).
+
+## Local Setup
+
+1. Start Redis:
+ ```bash
+ docker run -d --name redis-stack-server -p 6379:6379 redis/redis-stack-server:latest
+ ```
+2. Set `REDIS_URL` in your `.env` file:
+ ```env
+ REDIS_URL=redis://localhost:6379
+ ```
+3. Start the worker process:
+ ```bash
+ npm run worker:start
+ ```
diff --git a/docs/queues/ThumbnailGeneration.md b/docs/queues/ThumbnailGeneration.md
new file mode 100644
index 00000000..b7296384
--- /dev/null
+++ b/docs/queues/ThumbnailGeneration.md
@@ -0,0 +1,11 @@
+# ThumbnailGeneration BullMQ Queue
+
+The `ThumbnailGeneration` queue is responsible for offloading heavy image processing tasks (such as resizing user avatars and generating preview thumbnails for image attachments) to background workers.
+
+## Architecture
+
+1. **Producer**: Fastify route handles the direct file upload to S3-compatible storage, then pushes a job containing the `objectKey` to the BullMQ queue.
+2. **Worker**: The worker downloads the source image, uses `sharp` to generate optimized thumbnails (e.g. 100x100 webp), uploads the derivatives back to storage, and updates the database record.
+
+## Concurrency
+To prevent CPU starvation, the ThumbnailGeneration worker is configured with a concurrency limit of `5`.
diff --git a/package-lock.json b/package-lock.json
index e49d560b..5e770ae2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -99,7 +99,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -419,7 +418,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -458,7 +456,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -2912,7 +2909,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
- "peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -3420,7 +3416,6 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.2.tgz",
"integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.58.2",
"@typescript-eslint/types": "8.58.2",
@@ -3955,7 +3950,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4511,7 +4505,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -5014,8 +5007,7 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
@@ -5363,7 +5355,6 @@
"resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz",
"integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==",
"license": "Apache-2.0",
- "peer": true,
"peerDependencies": {
"@aws-sdk/client-rds-data": ">=3",
"@cloudflare/workers-types": ">=4",
@@ -5807,7 +5798,6 @@
"integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -5981,7 +5971,6 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -7656,7 +7645,6 @@
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"jiti": "bin/jiti.js"
}
@@ -8772,7 +8760,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -9104,7 +9091,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -9117,7 +9103,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -10247,7 +10232,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -10393,7 +10377,6 @@
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5"
@@ -10995,7 +10978,6 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -11530,7 +11512,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -11614,7 +11595,6 @@
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
diff --git a/packages/client/src/app/(app)/profile/page.tsx b/packages/client/src/app/(app)/profile/page.tsx
index ed676909..f75d5794 100644
--- a/packages/client/src/app/(app)/profile/page.tsx
+++ b/packages/client/src/app/(app)/profile/page.tsx
@@ -37,7 +37,29 @@ export default function ProfilePage() {
}
}, [mounted, isAuthenticated, router, user]);
- if (!mounted || !user) return null;
+ if (!mounted || !user) {
+ return (
+
+ );
+ }
const handleImageUpload = (e: React.ChangeEvent) => {
const file = e.target.files?.[0];
diff --git a/packages/client/src/app/auth/loading.tsx b/packages/client/src/app/auth/loading.tsx
new file mode 100644
index 00000000..4e2d55a8
--- /dev/null
+++ b/packages/client/src/app/auth/loading.tsx
@@ -0,0 +1,32 @@
+export default function Loading() {
+ return (
+
+ );
+}
diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts
index eecdbe00..8465ef32 100644
--- a/packages/server/src/app.ts
+++ b/packages/server/src/app.ts
@@ -12,6 +12,13 @@ import { dbTestRoutes } from "./routes/test-db";
import { authRoutes } from "./routes/auth";
import { roomRoutes } from "./routes/rooms";
import { waitlistRoutes } from "./routes/waitlist";
+import { userRoutes } from "./routes/users";
+import { groupRoutes } from "./routes/groups";
+import { messageRoutes } from "./routes/messages";
+import { locationRoutes } from "./routes/locations";
+import { circleRoutes } from "./routes/circles";
+import { postRoutes } from "./routes/posts";
+import { eventRoutes } from "./routes/events";
import { websocketRoutes } from "./websocket/wsHandler";
import fastifyJwt from "@fastify/jwt";
import fastifyCookie from "@fastify/cookie";
@@ -152,9 +159,17 @@ export const buildApp = async () => {
// --- Routes ---
app.register(healthRoutes, { prefix: "/api" });
app.register(dbTestRoutes, { prefix: "/api" });
- app.register(authRoutes, { prefix: "/api/auth" });
+ app.register(authRoutes, { prefix: "/api/v1/auth" });
+ app.register(groupRoutes, { prefix: "/api/v1/groups" });
+ app.register(messageRoutes, { prefix: "/api/v1/messages" });
+ app.register(locationRoutes, { prefix: "/api/v1/locations" });
+ app.register(userRoutes, { prefix: "/api/v1/users" });
+ app.register(circleRoutes, { prefix: "/api/v1/circles" });
+ app.register(postRoutes, { prefix: "/api/v1/posts" });
+ app.register(eventRoutes, { prefix: "/api/v1/events" });
app.register(roomRoutes, { prefix: "/api/rooms" });
app.register(waitlistRoutes, { prefix: "/api/waitlist" });
+ app.register(userRoutes, { prefix: "/api/users" });
app.register(websocketRoutes, { prefix: "/ws" });
// --- Observability: Per-Route HTTP Latency Tracking ---
diff --git a/packages/server/src/routes/circles.ts b/packages/server/src/routes/circles.ts
new file mode 100644
index 00000000..3a8bf7d1
--- /dev/null
+++ b/packages/server/src/routes/circles.ts
@@ -0,0 +1,18 @@
+import { FastifyInstance } from "fastify";
+import { db } from "../db/index";
+import { circles } from "@intellicircle/shared";
+import { eq } from "drizzle-orm";
+import { createSuccessResponse, createErrorResponse } from "../utils/response";
+
+export async function circleRoutes(app: FastifyInstance) {
+ app.get("/", async (request, reply) => {
+ try {
+ const results = await db.select().from(circles).limit(50);
+ return reply.send(createSuccessResponse(results));
+ } catch (error) {
+ app.log.error(error);
+ // Fix unhandled promise rejection by properly catching and returning 500
+ return reply.status(500).send(createErrorResponse("Internal server error", "INTERNAL_SERVER_ERROR"));
+ }
+ });
+}
diff --git a/packages/server/src/routes/events.ts b/packages/server/src/routes/events.ts
new file mode 100644
index 00000000..6c1ed170
--- /dev/null
+++ b/packages/server/src/routes/events.ts
@@ -0,0 +1,31 @@
+import { FastifyInstance } from "fastify";
+import { db } from "../db/index";
+import { events } from "@intellicircle/shared";
+import { createSuccessResponse, createErrorResponse } from "../utils/response";
+
+export async function eventRoutes(app: FastifyInstance) {
+ /**
+ * @api {get} /api/v1/events/nearby Request Nearby Events
+ * @apiName GetNearbyEvents
+ * @apiGroup Events
+ *
+ * @apiParam {Number} lat Latitude of the location.
+ * @apiParam {Number} lng Longitude of the location.
+ * @apiParam {Number} [radius=10] Search radius in kilometers.
+ *
+ * @apiSuccess {Object[]} events List of events.
+ */
+ app.get("/nearby", async (request, reply) => {
+ try {
+ // A simple implementation fetching first 50 events for scaffolding
+ const results = await db
+ .select()
+ .from(events)
+ .limit(50);
+ return reply.send(createSuccessResponse(results));
+ } catch (error) {
+ app.log.error(error);
+ return reply.status(500).send(createErrorResponse("Internal server error", "INTERNAL_SERVER_ERROR"));
+ }
+ });
+}
diff --git a/packages/server/src/routes/groups.ts b/packages/server/src/routes/groups.ts
new file mode 100644
index 00000000..92e84d34
--- /dev/null
+++ b/packages/server/src/routes/groups.ts
@@ -0,0 +1,8 @@
+import { FastifyInstance } from "fastify";
+import { createSuccessResponse, createErrorResponse } from "../utils/response";
+
+export async function groupRoutes(app: FastifyInstance) {
+ app.get("/", async (request, reply) => {
+ return reply.send(createSuccessResponse([]));
+ });
+}
diff --git a/packages/server/src/routes/locations.ts b/packages/server/src/routes/locations.ts
new file mode 100644
index 00000000..a14bb69a
--- /dev/null
+++ b/packages/server/src/routes/locations.ts
@@ -0,0 +1,8 @@
+import { FastifyInstance } from "fastify";
+import { createSuccessResponse, createErrorResponse } from "../utils/response";
+
+export async function locationRoutes(app: FastifyInstance) {
+ app.get("/", async (request, reply) => {
+ return reply.send(createSuccessResponse([]));
+ });
+}
diff --git a/packages/server/src/routes/messages.ts b/packages/server/src/routes/messages.ts
new file mode 100644
index 00000000..928f8a8b
--- /dev/null
+++ b/packages/server/src/routes/messages.ts
@@ -0,0 +1,8 @@
+import { FastifyInstance } from "fastify";
+import { createSuccessResponse, createErrorResponse } from "../utils/response";
+
+export async function messageRoutes(app: FastifyInstance) {
+ app.get("/", async (request, reply) => {
+ return reply.send(createSuccessResponse([]));
+ });
+}
diff --git a/packages/server/src/routes/posts.ts b/packages/server/src/routes/posts.ts
new file mode 100644
index 00000000..af442e4d
--- /dev/null
+++ b/packages/server/src/routes/posts.ts
@@ -0,0 +1,28 @@
+import { FastifyInstance } from "fastify";
+import { db } from "../db/index";
+import { posts } from "@intellicircle/shared";
+import { eq } from "drizzle-orm";
+import { createSuccessResponse, createErrorResponse } from "../utils/response";
+
+export async function postRoutes(app: FastifyInstance) {
+ app.get("/:id", async (request, reply) => {
+ const { id } = request.params as { id: string };
+
+ try {
+ const results = await db
+ .select()
+ .from(posts)
+ .where(eq(posts.id, parseInt(id, 10)))
+ .limit(1);
+
+ if (results.length === 0) {
+ return reply.status(404).send(createErrorResponse("Post not found", "POST_NOT_FOUND"));
+ }
+
+ return reply.send(createSuccessResponse(results[0]));
+ } catch (error) {
+ app.log.error(error);
+ return reply.status(500).send(createErrorResponse("Internal server error", "INTERNAL_SERVER_ERROR"));
+ }
+ });
+}
diff --git a/packages/server/src/routes/users.ts b/packages/server/src/routes/users.ts
new file mode 100644
index 00000000..a5d9f4b5
--- /dev/null
+++ b/packages/server/src/routes/users.ts
@@ -0,0 +1,34 @@
+import { FastifyInstance } from "fastify";
+import { db } from "../db/index";
+import { users } from "@intellicircle/shared";
+import { eq } from "drizzle-orm";
+import { createSuccessResponse, createErrorResponse } from "../utils/response";
+
+export async function userRoutes(app: FastifyInstance) {
+ // 1. Fetch User details by username
+ app.get("/:username", async (request, reply) => {
+ const { username } = request.params as { username: string };
+
+ try {
+ const userResults = await db
+ .select({
+ id: users.id,
+ username: users.username,
+ role: users.role,
+ createdAt: users.createdAt,
+ })
+ .from(users)
+ .where(eq(users.username, username))
+ .limit(1);
+
+ if (userResults.length === 0) {
+ return reply.status(404).send(createErrorResponse("User not found", "USER_NOT_FOUND"));
+ }
+
+ return reply.send(createSuccessResponse(userResults[0]));
+ } catch (error) {
+ app.log.error(error);
+ return reply.status(500).send(createErrorResponse("Internal server error", "INTERNAL_SERVER_ERROR"));
+ }
+ });
+}
diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts
index 62ef36e6..686fbd9e 100644
--- a/packages/shared/src/index.ts
+++ b/packages/shared/src/index.ts
@@ -1,15 +1 @@
-export {
- waitlist,
- users,
- chatRooms,
- messages,
- participants,
- authAuditLogs,
- insertWaitlistSchema,
- selectWaitlistSchema,
- anonymousAuthSchema,
- upgradeAuthSchema,
- loginAuthSchema,
- createRoomSchema,
- nearbyRoomsQuerySchema
-} from "./schema";
+export * from "./schema";
diff --git a/packages/shared/src/schema.ts b/packages/shared/src/schema.ts
index 097042a3..0e6fa7fa 100644
--- a/packages/shared/src/schema.ts
+++ b/packages/shared/src/schema.ts
@@ -8,6 +8,7 @@ import {
integer,
uniqueIndex,
index,
+ doublePrecision,
} from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { z } from "zod";
@@ -94,6 +95,20 @@ export const participants = pgTable(
}
);
+// --- WebSocket Schemas ---
+export const userJoinedPayloadSchema = z.object({
+ userId: z.number(),
+ roomId: z.number(),
+ timestamp: z.date().default(() => new Date()),
+});
+
+// --- Location Schemas ---
+export const locationCreationPayloadSchema = z.object({
+ label: z.string().min(1).max(100, "Label must be less than 100 characters"),
+ lat: z.number().min(-90).max(90, "Latitude must be between -90 and 90"),
+ lng: z.number().min(-180).max(180, "Longitude must be between -180 and 180"),
+});
+
// Waitlist Schemas
export const insertWaitlistSchema = z.object({
email: z.string().email(),
@@ -153,3 +168,29 @@ export const nearbyRoomsQuerySchema = z.object({
interests: z.union([z.string(), z.array(z.string())]).optional()
.transform(val => Array.isArray(val) ? val : (val ? [val] : []))
});
+
+
+export const circles = pgTable("circles", {
+ id: serial("id").primaryKey(),
+ name: varchar("name", { length: 256 }).notNull(),
+ description: text("description"),
+ creatorId: integer("creator_id").notNull(),
+ createdAt: timestamp("created_at").notNull().defaultNow(),
+ updatedAt: timestamp("updated_at").notNull().defaultNow().$onUpdate(() => new Date()),
+});
+
+export const posts = pgTable("posts", {
+ id: serial("id").primaryKey(),
+ content: text("content").notNull(),
+ authorId: integer("author_id").notNull(),
+ createdAt: timestamp("created_at").notNull().defaultNow(),
+});
+
+export const events = pgTable("events", {
+ id: serial("id").primaryKey(),
+ name: varchar("name", { length: 256 }).notNull(),
+ latitude: doublePrecision("latitude").notNull(),
+ longitude: doublePrecision("longitude").notNull(),
+ date: timestamp("date").notNull(),
+ createdAt: timestamp("created_at").notNull().defaultNow(),
+});