Replace PostgreSQL and Docker deployment with embedded Turso, Drizzle relational queries v2, native systemd deployment on port 43821, and fresh SQLite migrations.

Hop-State: A_06FP5KEDBTB4A9ZT7QB498G
Hop-Proposal: R_06FP5KDWMCKVVMQZT4MVZ28
Hop-Task: T_06FP5DZ7T0G45FG93PT90B8
Hop-Attempt: AT_06FP5DZ7T0PKQW99V27JCV8
This commit is contained in:
2026-07-14 15:44:42 -07:00
committed by Hop
184 changed files with 8375 additions and 53944 deletions
+5 -11
View File
@@ -13,7 +13,7 @@ export async function GET(req: NextRequest, context: RouteContext) {
const { handle } = await context.params;
const targetUser = await db.query.users.findFirst({
where: eq(users.handle, handle),
where: { handle: handle },
});
if (!targetUser) {
@@ -21,10 +21,7 @@ export async function GET(req: NextRequest, context: RouteContext) {
}
const block = await db.query.blocks.findFirst({
where: and(
eq(blocks.userId, currentUser.id),
eq(blocks.blockedUserId, targetUser.id)
),
where: { AND: [{ userId: currentUser.id }, { blockedUserId: targetUser.id }] },
});
return NextResponse.json({ blocked: !!block });
@@ -44,7 +41,7 @@ export async function POST(req: NextRequest, context: RouteContext) {
const { handle } = await context.params;
const targetUser = await db.query.users.findFirst({
where: eq(users.handle, handle),
where: { handle: handle },
});
if (!targetUser) {
@@ -57,10 +54,7 @@ export async function POST(req: NextRequest, context: RouteContext) {
// Check if already blocked
const existing = await db.query.blocks.findFirst({
where: and(
eq(blocks.userId, currentUser.id),
eq(blocks.blockedUserId, targetUser.id)
),
where: { AND: [{ userId: currentUser.id }, { blockedUserId: targetUser.id }] },
});
if (existing) {
@@ -104,7 +98,7 @@ export async function DELETE(req: NextRequest, context: RouteContext) {
const { handle } = await context.params;
const targetUser = await db.query.users.findFirst({
where: eq(users.handle, handle),
where: { handle: handle },
});
if (!targetUser) {
+14 -32
View File
@@ -37,10 +37,7 @@ export async function GET(request: Request, context: RouteContext) {
}
const targetHandle = `${remote.handle}@${remote.domain}`;
const existingRemoteFollow = await db.query.remoteFollows.findFirst({
where: and(
eq(remoteFollows.followerId, currentUser.id),
eq(remoteFollows.targetHandle, targetHandle)
),
where: { AND: [{ followerId: currentUser.id }, { targetHandle: targetHandle }] },
});
return NextResponse.json({ following: !!existingRemoteFollow, remote: true });
}
@@ -50,7 +47,7 @@ export async function GET(request: Request, context: RouteContext) {
}
const targetUser = await db.query.users.findFirst({
where: eq(users.handle, cleanHandle),
where: { handle: cleanHandle },
});
if (!targetUser) {
@@ -65,10 +62,7 @@ export async function GET(request: Request, context: RouteContext) {
}
const existingFollow = await db.query.follows.findFirst({
where: and(
eq(follows.followerId, currentUser.id),
eq(follows.followingId, targetUser.id)
),
where: { AND: [{ followerId: currentUser.id }, { followingId: targetUser.id }] },
});
return NextResponse.json({ following: !!existingFollow });
@@ -96,7 +90,7 @@ export async function POST(request: Request, context: RouteContext) {
const { handle } = await context.params;
const cleanHandle = handle.toLowerCase().replace(/^@/, '');
const remote = parseRemoteHandle(handle);
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
if (currentUser.isSuspended || currentUser.isSilenced) {
return NextResponse.json({ error: 'Account restricted' }, { status: 403 });
@@ -107,10 +101,7 @@ export async function POST(request: Request, context: RouteContext) {
// Check if already following
const existingRemoteFollow = await db.query.remoteFollows.findFirst({
where: and(
eq(remoteFollows.followerId, currentUser.id),
eq(remoteFollows.targetHandle, targetHandle)
),
where: { AND: [{ followerId: currentUser.id }, { targetHandle: targetHandle }] },
});
if (existingRemoteFollow) {
return NextResponse.json({ error: 'Already following' }, { status: 400 });
@@ -179,7 +170,7 @@ export async function POST(request: Request, context: RouteContext) {
// Find target user
const targetUser = await db.query.users.findFirst({
where: eq(users.handle, cleanHandle),
where: { handle: cleanHandle },
});
if (!targetUser) {
@@ -196,10 +187,7 @@ export async function POST(request: Request, context: RouteContext) {
// Check if already following
const existingFollow = await db.query.follows.findFirst({
where: and(
eq(follows.followerId, currentUser.id),
eq(follows.followingId, targetUser.id)
),
where: { AND: [{ followerId: currentUser.id }, { followingId: targetUser.id }] },
});
if (existingFollow) {
@@ -268,7 +256,7 @@ export async function DELETE(request: Request, context: RouteContext) {
const { handle } = await context.params;
const cleanHandle = handle.toLowerCase().replace(/^@/, '');
const remote = parseRemoteHandle(handle);
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
if (remote) {
if (!db) {
@@ -276,10 +264,7 @@ export async function DELETE(request: Request, context: RouteContext) {
}
const targetHandle = `${remote.handle}@${remote.domain}`;
const existingRemoteFollow = await db.query.remoteFollows.findFirst({
where: and(
eq(remoteFollows.followerId, currentUser.id),
eq(remoteFollows.targetHandle, targetHandle)
),
where: { AND: [{ followerId: currentUser.id }, { targetHandle: targetHandle }] },
});
if (!existingRemoteFollow) {
return NextResponse.json({ error: 'Not following' }, { status: 400 });
@@ -306,7 +291,7 @@ export async function DELETE(request: Request, context: RouteContext) {
// Update the user's following count (atomic decrement, clamped to 0)
await db.update(users)
.set({ followingCount: sql`GREATEST(0, ${users.followingCount} - 1)` })
.set({ followingCount: sql`max(0, ${users.followingCount} - 1)` })
.where(eq(users.id, currentUser.id));
console.log(`[Swarm] Unfollow delivered to ${remote.domain}`);
@@ -319,7 +304,7 @@ export async function DELETE(request: Request, context: RouteContext) {
// Find target user
const targetUser = await db.query.users.findFirst({
where: eq(users.handle, cleanHandle),
where: { handle: cleanHandle },
});
if (!targetUser) {
@@ -331,10 +316,7 @@ export async function DELETE(request: Request, context: RouteContext) {
// Find existing follow
const existingFollow = await db.query.follows.findFirst({
where: and(
eq(follows.followerId, currentUser.id),
eq(follows.followingId, targetUser.id)
),
where: { AND: [{ followerId: currentUser.id }, { followingId: targetUser.id }] },
});
if (!existingFollow) {
@@ -346,11 +328,11 @@ export async function DELETE(request: Request, context: RouteContext) {
// Update counts (atomic decrements, clamped to 0)
await db.update(users)
.set({ followingCount: sql`GREATEST(0, ${users.followingCount} - 1)` })
.set({ followingCount: sql`max(0, ${users.followingCount} - 1)` })
.where(eq(users.id, currentUser.id));
await db.update(users)
.set({ followersCount: sql`GREATEST(0, ${users.followersCount} - 1)` })
.set({ followersCount: sql`max(0, ${users.followersCount} - 1)` })
.where(eq(users.id, targetUser.id));
return NextResponse.json({ success: true, following: false });
@@ -61,7 +61,7 @@ export async function GET(request: Request, context: RouteContext) {
// Find the user
const user = await db.query.users.findFirst({
where: eq(users.handle, cleanHandle),
where: { handle: cleanHandle },
});
if (!user) {
@@ -94,7 +94,7 @@ export async function GET(request: Request, context: RouteContext) {
// Get remote followers
const userRemoteFollowers = await db.query.remoteFollowers.findMany({
where: eq(remoteFollowers.userId, user.id),
where: { userId: user.id },
limit,
});
@@ -61,7 +61,7 @@ export async function GET(request: Request, context: RouteContext) {
// Find the user
const user = await db.query.users.findFirst({
where: eq(users.handle, cleanHandle),
where: { handle: cleanHandle },
});
if (!user) {
@@ -94,7 +94,7 @@ export async function GET(request: Request, context: RouteContext) {
// Get remote following
const userRemoteFollowing = await db.query.remoteFollows.findMany({
where: eq(remoteFollows.followerId, user.id),
where: { followerId: user.id },
limit,
});
+7 -14
View File
@@ -108,7 +108,7 @@ export async function GET(request: Request, context: RouteContext) {
// Find the user
const user = await db.query.users.findFirst({
where: eq(users.handle, cleanHandle),
where: { handle: cleanHandle },
});
const isRemotePlaceholder = user && cleanHandle.includes('@');
@@ -141,13 +141,13 @@ export async function GET(request: Request, context: RouteContext) {
// Get user's liked posts
const userLikes = await db.query.likes.findMany({
where: eq(likes.userId, user.id),
where: { userId: user.id },
with: {
post: {
with: likedPostRelations,
},
},
orderBy: [desc(likes.createdAt)],
orderBy: () => [desc(likes.createdAt)],
limit,
});
@@ -156,8 +156,8 @@ export async function GET(request: Request, context: RouteContext) {
.map(like => like.post);
const swarmLikedRows = await db.query.userSwarmLikes.findMany({
where: eq(userSwarmLikes.userId, user.id),
orderBy: [desc(userSwarmLikes.likedAt)],
where: { userId: user.id },
orderBy: () => [desc(userSwarmLikes.likedAt)],
limit,
});
@@ -225,19 +225,12 @@ export async function GET(request: Request, context: RouteContext) {
if (localPostIds.length > 0) {
const viewerLikes = await db.query.likes.findMany({
where: and(
eq(likes.userId, viewer.id),
inArray(likes.postId, localPostIds)
),
where: { AND: [{ userId: viewer.id }, { postId: { in: localPostIds } }] },
});
const likedPostIds = new Set(viewerLikes.map(l => l.postId));
const viewerReposts = await db.query.posts.findMany({
where: and(
eq(posts.userId, viewer.id),
inArray(posts.repostOfId, localPostIds),
eq(posts.isRemoved, false)
),
where: { AND: [{ userId: viewer.id }, { repostOfId: { in: localPostIds } }, { isRemoved: false }] },
});
const repostedPostIds = new Set(viewerReposts.map(r => r.repostOfId));
+20 -38
View File
@@ -151,13 +151,13 @@ async function getMixedProfileCursorDate(cursor: string | null) {
if (cursor.startsWith('swarm-repost:')) {
const repostRow = await db.query.userSwarmReposts.findFirst({
where: eq(userSwarmReposts.id, cursor.replace('swarm-repost:', '')),
where: { id: cursor.replace('swarm-repost:', '') },
});
return repostRow?.repostedAt ?? null;
}
const cursorPost = await db.query.posts.findFirst({
where: eq(posts.id, cursor),
where: { id: cursor },
});
return cursorPost?.createdAt ?? null;
}
@@ -173,7 +173,7 @@ async function populateViewerLikeState(
const { getSession } = await import('@/lib/auth');
const session = await getSession();
const viewer = session?.user;
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
if (!viewer) {
return remotePosts;
@@ -301,7 +301,7 @@ export async function GET(request: Request, context: RouteContext) {
// Find the user
const user = await db.query.users.findFirst({
where: eq(users.handle, cleanHandle),
where: { handle: cleanHandle },
});
const isRemotePlaceholder = user && cleanHandle.includes('@');
@@ -330,39 +330,28 @@ export async function GET(request: Request, context: RouteContext) {
// Get user's posts with cursor-based pagination
const cursorDate = await getMixedProfileCursorDate(cursor);
let whereConditions = and(
eq(posts.userId, user.id),
eq(posts.isRemoved, false),
isNull(posts.replyToId),
isNull(posts.swarmReplyToId)
);
if (cursorDate) {
whereConditions = and(
eq(posts.userId, user.id),
eq(posts.isRemoved, false),
isNull(posts.replyToId),
isNull(posts.swarmReplyToId),
lt(posts.createdAt, cursorDate)
);
}
const whereConditions = {
userId: user.id,
isRemoved: false,
replyToId: { isNull: true as const },
swarmReplyToId: { isNull: true as const },
...(cursorDate ? { createdAt: { lt: cursorDate } } : {}),
};
const localPosts = await db.query.posts.findMany({
where: whereConditions,
with: userPostRelations,
orderBy: [desc(posts.createdAt)],
orderBy: () => [desc(posts.createdAt)],
limit: cursor ? limit : limit * 2,
});
const swarmRepostWhere = cursorDate
? and(
eq(userSwarmReposts.userId, user.id),
lt(userSwarmReposts.repostedAt, cursorDate)
)
: eq(userSwarmReposts.userId, user.id);
const swarmRepostWhere = {
userId: user.id,
...(cursorDate ? { repostedAt: { lt: cursorDate } } : {}),
};
const swarmRepostRows = await db.query.userSwarmReposts.findMany({
where: swarmRepostWhere,
orderBy: [desc(userSwarmReposts.repostedAt)],
orderBy: () => [desc(userSwarmReposts.repostedAt)],
limit: cursor ? limit : limit * 2,
});
let userPosts: any[] = [
@@ -400,19 +389,12 @@ export async function GET(request: Request, context: RouteContext) {
if (localPostIds.length > 0) {
const viewerLikes = await db.query.likes.findMany({
where: and(
eq(likes.userId, viewer.id),
inArray(likes.postId, localPostIds)
),
where: { AND: [{ userId: viewer.id }, { postId: { in: localPostIds } }] },
});
viewerLikes.forEach((like) => likedPostIds.add(like.postId));
const viewerReposts = await db.query.posts.findMany({
where: and(
eq(posts.userId, viewer.id),
inArray(posts.repostOfId, localPostIds),
eq(posts.isRemoved, false)
),
where: { AND: [{ userId: viewer.id }, { repostOfId: { in: localPostIds } }, { isRemoved: false }] },
});
viewerReposts.forEach((repost) => {
if (repost.repostOfId) {
@@ -422,7 +404,7 @@ export async function GET(request: Request, context: RouteContext) {
}
if (swarmTargets.length > 0) {
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
const likedIds = await getViewerSwarmLikedPostIds(
swarmTargets.map((post) => ({
id: post.id,
+16 -24
View File
@@ -95,7 +95,7 @@ export async function GET(request: Request, context: RouteContext) {
}
const user = await db.query.users.findFirst({
where: eq(users.handle, cleanHandle),
where: { handle: cleanHandle },
});
const isRemotePlaceholder = user && cleanHandle.includes('@');
@@ -121,30 +121,29 @@ export async function GET(request: Request, context: RouteContext) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
let whereConditions = and(
eq(posts.userId, user.id),
eq(posts.isRemoved, false),
or(isNotNull(posts.replyToId), isNotNull(posts.swarmReplyToId)),
);
let cursorDate: Date | undefined;
if (cursor) {
const cursorPost = await db.query.posts.findFirst({
where: eq(posts.id, cursor),
where: { id: cursor },
});
if (cursorPost) {
whereConditions = and(
eq(posts.userId, user.id),
eq(posts.isRemoved, false),
or(isNotNull(posts.replyToId), isNotNull(posts.swarmReplyToId)),
lt(posts.createdAt, cursorPost.createdAt),
);
cursorDate = cursorPost.createdAt;
}
}
let replyPosts: any[] = await db.query.posts.findMany({
where: whereConditions,
where: {
userId: user.id,
isRemoved: false,
OR: [
{ replyToId: { isNotNull: true } },
{ swarmReplyToId: { isNotNull: true } },
],
...(cursorDate ? { createdAt: { lt: cursorDate } } : {}),
},
with: replyRelations,
orderBy: [desc(posts.createdAt)],
orderBy: () => [desc(posts.createdAt)],
limit,
});
@@ -158,21 +157,14 @@ export async function GET(request: Request, context: RouteContext) {
const viewerLikes = postIds.length > 0
? await db.query.likes.findMany({
where: and(
eq(likes.userId, viewer.id),
inArray(likes.postId, postIds),
),
where: { AND: [{ userId: viewer.id }, { postId: { in: postIds } }] },
})
: [];
const likedPostIds = new Set(viewerLikes.map((like) => like.postId));
const viewerReposts = postIds.length > 0
? await db.query.posts.findMany({
where: and(
eq(posts.userId, viewer.id),
inArray(posts.repostOfId, postIds),
eq(posts.isRemoved, false),
),
where: { AND: [{ userId: viewer.id }, { repostOfId: { in: postIds } }, { isRemoved: false }] },
})
: [];
const repostedPostIds = new Set(viewerReposts.map((post) => post.repostOfId));
+3 -6
View File
@@ -32,7 +32,7 @@ export async function GET(request: Request, context: RouteContext) {
}
const user = await db.query.users.findFirst({
where: eq(users.handle, cleanHandle),
where: { handle: cleanHandle },
});
// If user exists but is a remote placeholder (handle contains @), fetch fresh data from remote
@@ -147,10 +147,7 @@ export async function GET(request: Request, context: RouteContext) {
canReceiveDms = true; // Can DM yourself
} else {
const isFollowingViewer = await db.query.follows.findFirst({
where: and(
eq(follows.followerId, user.id),
eq(follows.followingId, session.user.id)
)
where: { AND: [{ followerId: user.id }, { followingId: session.user.id }] }
});
if (isFollowingViewer) {
canReceiveDms = true;
@@ -163,7 +160,7 @@ export async function GET(request: Request, context: RouteContext) {
// If this is a bot, include owner info
if (user.isBot && user.botOwnerId) {
const owner = await db.query.users.findFirst({
where: eq(users.id, user.botOwnerId),
where: { id: user.botOwnerId },
});
if (owner) {
userResponse.botOwner = {