[nextjs]SNS Server-24(myapp34)Β 

πŸ‘‰πŸ» νšŒμ›κ°€μž… 및 둜그인 κΈ°λŠ₯μž…λ‹ˆλ‹€.
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

Mastodon
GTS(freelifemakers.com)

βœ”οΈ 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.
local server profile

πŸ“ μ½”λ“œμˆ˜μ • / 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> 
local server posts

 βœ”️ 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.
Timeline

πŸ“ 리λͺ¨νŠΈ μ„œλ²„ 상세 νŽ˜μ΄μ§€ μž„μ‹œλ°μ΄ν„°
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.

remote mastodon

πŸ“ 전체 κ³Όμ • μš”μ•½ / 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”

Leave a Reply