ππ» νμκ°μ
λ° λ‘κ·ΈμΈ κΈ°λ₯μ
λλ€.
These are the sign-up and login functions.
ππ» λ‘κ·ΈμΈ ν pinaforeμμ λ΄ μλ²(aloy-horizon.duckdns.org)κ° κ²μλλλ‘ ν©λλ€.
After logging in, ensure that my server (aloy-horizon.duckdns.org) is discoverable in Pinafore.
ππ» λ‘κ·ΈμΈ ν pinafore νμλΌμΈμμ λ΄ μλ²μ κΈμ λ³Ό μ μμ΅λλ€.
After logging in, you can view posts from your server on the Pinafore timeline.
ππ» μ 체 μ½λλ κΉνλΈμμ νμΈ ν μ μμ΅λλ€.
You can find the full code on GitHub.
https://github.com/gideonslife01/flm-nextjs
π μ 체 νλ‘μ νΈ κ΅¬μ‘° / Overall Project Structure
myapp project/
βββ app/ (Next.js App Router)
β βββ .well-known/webfinger/route.ts -> webfinger
β βββ api/follow/route.ts -> Follow API(temporary)
β βββ api/announce/route.ts -> Boost(Announcement)
β βββ api/like/route.ts. -> Like API
β βββ api/posts/route.ts -> Writing API
β βββ api/timeline/route.ts -> Timeline API
β βββ api/v1/instance/route.ts -> auth
β βββ api/v1/apps/route.ts. -> auth
β βββ api/v1/accounts/verify_credentials/route.ts -> auth
β βββ api/v1/statuses/route.ts -> Write Post
β βββ api/v1/timelines/home/route.ts -> timeline
β βββ oauth/authorize/route.ts -> auth
β βββ oauth/token/route.ts -> auth
β βββ api/v1/search/home/route.ts -> search
β βββ api/v2/search/home/route.ts -> search
β βββ users/[username]/
β β βββ statuses/[id]/route.ts -> Indivisual Post
β β βββ route.ts -> Acotr Information
β β βββ followers/route.ts -> Followers List
β β βββ following/route.ts -> Following List
β β βββ inbox/route.ts -> Inbox
β β βββ outbox/route.ts -> outbox
β βββ usersui/[username]/
β β βββ page.tsx -> Timeline UI
β β βββ _components/themes/
β β βββ themeex/ThemeexTheme.tsx -> Example Theme
β β βββ pinafore/PinaforeTheme.tsx -> Theme 1
β β βββ mastodon/MastodonTheme.tsx -> Theme 2
β β βββ minimal/MinimaltTheme.tsx -> Theme 3
β βββ layout.tsx, page.tsx, globals.css
β βββ favicon.ico
βββ lib/
β βββ theme.tsx -> Theme Provider
β βββ watchThemes.ts -> Check real-time theme changes
β βββ auth.ts -> Authentication, User Management
β βββ ap.ts -> Follow,Undo,Create,Likes,Announce
β βββ db.ts -> DB connection
βββ data/
β βββ keys/userIDs/ -> private.pem, public.pem(New)
β βββ keys/ -> private.pem, public.pem(legacy)
βββ data.sqlite -> Database(1/3)
βββ data.sqlite-wal -> Database(2/3)
βββ data.sqlite-shm -> Database(3/3)
βββ Caddyfile -> https
βββ instrumentation.ts -> Background Server
βββ package.json
π νλ‘μ νΈ μμ / Project Start
π μλμμ±λ ν μ΄λΈ / Automatically generated table
CREATE TABLE outbox (
id TEXT PRIMARY KEY,
type TEXT,
actor TEXT,
object TEXT,
created_at INTEGER
);
π κ²μκΈ°λ₯ λΌμ°νΈ μΆκ° / Add search function route
βοΈ app/api/v1/search/route.ts – νμ νΈνμ© / For backward compatibility
# v2 μ½λμ λμΌν¨ / Same as the v2 code.
βοΈ app/api/v2/search/route.ts – μ€μ μ¬μ©λλ λΌμ°νΈ / Routes actually used
// β
myapp34
import { NextResponse } from 'next/server';
import db from '@/lib/db';
const DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org';
export async function GET(req: Request) { ... }
βοΈ λ‘컬 μλ² ν μ€νΈ / Local Server Test(aloy-horizon.duckdns.org)
— V1, V2 λͺ¨λ λμΌν μ½λ μ¬μ©νλ©΄ λ¨
You can use the same code for both V1 and V2.
— V1
curl "https://aloy-horizon.duckdns.org/api/v1/search?q=user1" -H "Authorization: Bearer YOUR_TOKEN"
curl "https://aloy-horizon.duckdns.org/api/v1/search?q=user1" -H "4847d5c9-efdd-450f-b724-01c28676d6ea433c1b89-a4ac-437b-a41a-39fc2e6bdfa6"
{"accounts":[{"id":"1","username":"user1","acct":"user1@aloy-horizon.duckdns.org","display_name":"user1","url":"https://aloy-horizon.duckdns.org/users/user1","avatar":"https://aloy-horizon.duckdns.org/icon.png"}],"statuses":[],"hashtags":[]}%
— V2
curl "https://aloy-horizon.duckdns.org/api/v2/search?q=user1" -H "Authorization: Bearer YOUR_TOKEN"
gimdaegyeong@gimdaegyeong-ui-MacBookAir ~ % curl "https://aloy-horizon.duckdns.org/api/v2/search?q=user1" -H "Authorization: Bearer 4847d5c9-efdd-450f-b724-01c28676d6ea433c1b89-a4ac-437b-a41a-39fc2e6bdfa6"
{"accounts":[{"id":"1","username":"user1","acct":"user1@aloy-horizon.duckdns.org","display_name":"user1","url":"https://aloy-horizon.duckdns.org/users/user1","avatar":"https://aloy-horizon.duckdns.org/icon.png"}],"statuses":[],"hashtags":[]}%
βοΈ λ¦¬λͺ¨νΈ μλ² ν μ€νΈ / Remote Server Test
— pinafore.socialμμ μ§μ κ²μ / Search directly on pinafore.social


βοΈ app/api/v1/accounts/[id]/route.ts – νλ‘ν μ 보 / Profile information
— 리λͺ¨νΈ μλ²λ pinafore.socialμμ κ²μν μ μ λ₯Ό μλ―Έν©λλ€.
A remote server refers to a user found on pinafore.social.
— λ‘컬μλ²(aloy-horizon),리λͺ¨νΈμλ²(λ€λ₯Έ μλ²μ μ μ )
Local server (aloy-horizon), remote server (user on another server)
//β
myapp34 - Profile info.
import { NextResponse } from 'next/server';
const DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org';
export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) { ... ...
// β
리λͺ¨νΈ/remote!
if (id.includes('mastodon.social') || id.includes('https') || id.startsWith('remote_')) { ... ...}
... ...
// β
λ‘컬/local
const username = rawId === '1'? 'user1' : rawId.replace('user', '')? `user${rawId}` : 'user1';
const headerUrl = `https://picsum.photos/seed/${username}_${DOMAIN}/1200/400`;
return NextResponse.json({ ... ... })
}
βοΈ app/api/v1/accounts/[id]/statuses/route.ts – μ μ κ° μμ±ν κΈ / User-written post
— 리λͺ¨νΈ μλ²λ pinafore.socialμμ κ²μν μ μ λ₯Ό μλ―Έν©λλ€.
A remote server refers to a user found on pinafore.social.
— λ‘컬μλ²(aloy-horizon),리λͺ¨νΈμλ²(λ€λ₯Έ μλ²μ μ μ )
Local server (aloy-horizon), remote server (user on another server)
// β
myapp34 - User-written post
import { NextResponse } from 'next/server';
import db from '@/lib/db';
const DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org';
export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
... ...
// β
리λͺ¨νΈ/remote (https:// λλ remote_ λλ mastodon ν¬ν¨!) /
if (id.startsWith('https://') || id.startsWith('remote_') || id.includes('mastodon.social')) { ... ...}
// β
λ‘컬/local
const username = rawId === '1'? 'user1' : `user${rawId}`;
console.log(`π λ‘컬! username=${username}`);
... ...
}
βοΈ ν μ€νΈ / Test
curl "https://aloy-horizon.duckdns.org/api/v1/accounts/1/statuses" -H "Authorization: Bearer TOKEN"
# ν°λ―Έλμμ μ€ν / Run in the terminal
curl "https://aloy-horizon.duckdns.org/api/v1/accounts/1/statuses" -H "Authorization: Bearer 4847d5c9-efdd-450f-b724-016d6eac2867a433c1b89-a4ac-437b-a41a-39fc2e6bdfa6"
# μλ΅ / response
[{"id":"73a7e43b-20000-40dc-3hj4kac-1aa123fda326","uri":"https://aloy-horizon.duckdns.org/users/user1/statuses/73a7e43b-24b4-40dc-8ebc-1aa123fda326","url":"https://aloy-horizon.duckdns.org/users/user1/statuses/73a7e43b-24b4-40dc-8ebc-1aa123fda326","account":{"id":"1","username":"user1","acct":"user1@aloy-horizon.duckdns.org","display_name":"user1","avatar":"https://aloy-horizon.duckdns.org/icon.png"},"content":"<p>aloy first pinafore.social post test</p>","created_at":"2026-09-06T23:06:23.998Z","visibility":"public","reblogs_count":0,"favourites_count":0},{"id":"4ace7a75-3a40-494f-8dea-bf3f688b6cfb", ... ...
# λλ¨Έμ§ λ°μ΄ν° μλ΅
The remaining data is omitted.

π μ½λμμ / Code Modification
βοΈ app/api/v1/statuses/route.ts
— κΈμ°κΈ° λΌμ°νΈ(DBμ μ₯ κΈ°λ₯ μΆκ°)
Writing route (added DB save functionality)
// app/api/v1/statuses/route.ts - κΈμ°κΈ° DBμ μ₯ / Creating status and saving to DB
// β
myapp34-2
import { NextResponse } from 'next/server';
import db from '@/lib/db';
import { randomUUID } from 'crypto';
const DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org';
export async function POST(req: Request) {
... ...
// posts table
db.prepare(`INSERT INTO posts (id, username, content, created_at) VALUES (?,?,?,?)`)
.run(id, username, content, now);
} catch (e) { console.error('posts μ μ₯ μλ¬/posts save error', e); }
... ...
// outbox table
db.prepare(`INSERT INTO outbox (id, type, actor, object, created_at) VALUES (?,?,?,?,?)`)
.run(id, 'Create', `https://${DOMAIN}/users/${username}`, JSON.stringify(note), now);
} catch (e) { console.error('outbox μ μ₯ μλ¬ / outbox save error', e); }
}
βοΈ ν μ€νΈ / Test
— pinafore.socialμμ κΈμ μμ±ν κ²½μ° μλ μ²λΌ DBμ μ μ₯λ©λλ€.
sqlite> select * from posts;
e6c99652-572f-491a-9e61-05e67b0d58fc|Hello! my SNS!!|2026-08-14 04:59:51|user1
dcece9ee-8d4a-4f63-91e1-22fc60513f62|Hello! my SNS!!|2026-08-14 05:00:18|user1
cc30330e-7040-4a1b-9e60-731066b8817e|Hello! my SNS3!!|2026-08-17 23:08:05|user1
e6d532c8-974b-4a01-89f6-6e1b2c4cff32|Hello Fediverse! #test|2026-08-20 02:16:04|user1
aea0ec89-a368-44a9-8493-b2bd82a797a8|post test! #test|2026-08-20 03:08:18|user1
9b2ead85-4b15-4df9-9de4-04c4a7fbcab4|Hello! Mastodon!!|2026-09-01 02:50:10|user1
aefc7ff8-5b38-4fb7-829d-9b14343cec6a|Hello local UI #apiposts|2026-09-05 03:39:37|user1
4ace7a75-3a40-494f-8dea-bf3f688b6cfb|Hello Activity UI #activityposts|2026-09-05 03:39:48|user1
73a7e43b-24b4-40dc-8ebc-1aa123fda326|aloy first pinafore.social post test|1788735983998|user1
sqlite>
sqlite> select * from outbox;
73a7e43b-24b4-40dc-8ebc-1aa123fda326|Create|https://aloy-horizon.duckdns.org/users/user1|{"id":"https://aloy-horizon.duckdns.org/users/user1/statuses/73a7e43b-24b4-40dc-8ebc-1aa123fda326","type":"Note","content":"<p>aloy first pinafore.social post test</p>","attributedTo":"https://aloy-horizon.duckdns.org/users/user1","published":"2026-09-06T23:06:23.998Z","to":["https://www.w3.org/ns/activitystreams#Public"]}|1788735983998
sqlite>

βοΈ app/api/v1/timelines/home/route.ts
— pinaforeμ 보μ¬μ€ νμλΌμΈ / Timeline to display on Pinafore
// β
myapp34 - app/api/v1/timelines/home/route.ts
// - pinafore ν νμλΌμΈ API / pinafore Home Timeline API
import { NextResponse } from 'next/server';
import db from '@/lib/db';
const DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org';
export async function GET(req: Request) { ... }
export async function OPTIONS() { ... }
— ν μ€νΈ / Test
curl "https://aloy-horizon.duckdns.org/api/v1/timelines/home" -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# ν°λ―Έλμμ μ€ν / Run in the terminal
% curl "https://aloy-horizon.duckdns.org/api/v1/timelines/home" -H "Authorization: Bearer 4847d5c9-efdd-450f-b724-01c28676d6ea433c1b89-a4ac-437b-a41a-39fc2e6bdfa6"
# μλ΅ / response
[{"id":"73a7e43b-0000-1234-0000-1000000","uri":"https://aloy-horizon.duckdns.org/users/user1/statuses/73a7e43b-24b4-40dc-8ebc-1aa123fda326","url":"https://aloy-horizon.duckdns.org/users/user1/statuses/73a7e43b-24b4-40dc-8ebc-1aa123fda326","account":{"id":"1","username":"user1","acct":"user1@aloy-horizon.duckdns.org","display_name":"user1","avatar":"https://aloy-horizon.duckdns.org/icon.png"},"content":"<p>aloy first pinafore.social post test</p>","created_at":"2026-09-06T23:06:23.998Z","visibility":"public","reblogs_count":0,"favourites_count":0},{"id":"4ace7a75-3a40-494f-8dea-bf3f688b6cfb", ... ...
# λλ¨Έμ§ λ°μ΄ν° μλ΅
The remaining data is omitted.

π 리λͺ¨νΈ μλ² μμΈ νμ΄μ§ μμλ°μ΄ν°
Remote Server Detail Page Placeholder Data
βοΈ μ§κΈ pinafore.socialμμ λ€λ₯Έμλ²(리λͺ¨νΈμλ²)λ₯Ό κ²μν΄μ μμΈνμ΄μ§λ₯Ό 보면 λ€μμ²λΌ μμλ°μ΄ν°κ° λ€μ΄κ° μμ΅λλ€.
If you search for another server (remote server) on pinafore.social right now and view its details page, you will see placeholder data like this.
βοΈ λ€μ νλ‘μ νΈμμ μμ μμ μ
λλ€.
This is scheduled to be modified in the next project.

π μ 체 κ³Όμ μμ½ / Summary of the entire process
1. κΈμ°κΈ° / Write Post
- Pinaforeμμ κΈμ°λ©΄ λ΄ DBμ μ μ₯λ¨.
When you write on Pinafore, it gets saved to my database.
- pinaforeμμ POST /api/v1/statuses
POST /api/v1/statuses in pinafore
- app/api/v1/statuses/route.ts
2. ν νμλΌμΈ / Home Timeline
- λ΄ κΈκ³Ό inboxκΈ(λ°μ κΈμ DBμμ κΊΌλ΄μ λ³΄μ¬ μ€.
My posts and inbox posts (retrieved from the database and displayed).
- Pinaforeμμ GET /api/v1/timelines/home
GET /api/v1/timelines/home in pinafore
- app/api/v1/timelines/home/route.ts
3. κ²μ / Searh
- pinaforeμμ κ²μ
Search in pinafore
- Pinaforeμμ GET /api/v2/search?q=david@freelifemakers.com
GET /api/v2/search?q=david@freelifemakers.com in pinafore
- webfinger + actor fetch! id=remote_freelifemakers_com_david 리ν΄/return
- app/api/v2/search/route.ts
4. κ²μν μ μ νλ‘ν / Searched user profile
- κ²μν μ μ μμΈ νμ΄μ§ νλ‘ν
Searched User Detail Page Profile
- Pinaforeμμ GET /api/v1/accounts/remote_freelifemakers_com_david
- app/api/v1/accounts/[id]/route.ts
5. κ²μν μ μ κΈ / Posts by the searched user
- κ²μν μ μ μμΈ νμ΄μ§ κΈ
Searched User Detail Page Profile
- Pinaforeμμ GET /api/v1/accounts/remote_freelifemakers_com_david/statuses
- app/api/v1/accounts/[id]/statuses/route.ts
μν볡μ 8μ₯ 32μ / John 8:32
“κ·Έλ¦¬κ³ λν¬λ μ§λ¦¬λ₯Ό μκ² λ κ²μ΄λ©°, μ§λ¦¬κ° λν¬λ₯Ό μμ λ‘κ² ν κ²μ΄λ€.”
“Then you will know the truth ,and the truth will set you free”