ππ» νμκ°μ
λ° λ‘κ·ΈμΈ κΈ°λ₯ ꡬν μ€ μ
λλ€.
We are currently implementing the sign-up and login functions.
ππ» public key κ²μ¦ λ‘μ§μ κ³μ μ§νν©λλ€.
We will proceed with the public key verification logic.
ππ» Inboxλ₯Ό public keyλ‘ κ²μ¦ν©λλ€.
Verify the Inbox using the public key.
ππ» inboxμ μλ²μ μλ ν₯μμ μν΄μ μΊμ κΈ°λ₯μ μΆκ°ν©λλ€.
A caching feature is being added to the inbox to improve server speed.
ππ» outboxλ μΊμκΈ°λ₯μ μ μ©νμ§ μμ΅λλ€.
The outbox does not utilize caching.
ππ» pinaforeμμ κΈ μμ±μ΄ κ°λ₯νλλ‘ outbox/route.tsμ POSTν¨μλ₯Ό μΆκ°ν©λλ€.
Add a POST function to outbox/route.ts to enable writing posts in Pinafore.
ππ» μ 체 μ½λλ κΉνλΈμμ νμΈ ν μ μμ΅λλ€.
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
β βββ 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
π Inbox Public Key Cache
βοΈ μλ² μλ ν₯μμ μν΄μ inboxμ μΊμκΈ°λ₯μ μΆκ°ν©λλ€.
A caching function is being added to the inbox to improve server speed.
βοΈ λ€λ₯Έ μλ² κΈμ 빨리 λ°μ μ μμ΅λλ€.
You can quickly receive posts from other servers.
— myapp32/lib/ap.ts
... ...
// β
myapp32 - 곡κ°ν€ μΊμ± / Public key caching
const publicKeyCache = new Map<string, { key: string, cachedAt: number }>();
const CACHE_TTL = 1000 * 60 * 60 * 1; // 1μκ°! (λΉ λ₯΄λ©΄μ μ΅μ !) / 1 hour! (fast and up-to-date!)
... ...
export async function fetchActorPublicKeyCached(actorUrl: string, username: string): Promise<string> { ... }
export async function fetchActorPublicKeyWithRetry(
actorUrl: string,
username: string,
verifyReq: Request
): Promise<{ publicKey: string, isValid: boolean }> { ... }
βοΈ inboxμ μΊμ μ μ©
Apply caching to the inbox.
— myapp32/app/users/[username]/inbox/route.ts
import { sendAccept, verifyHttpSignature, fetchActorPublicKey, fetchActorPublicKeyWithRetry } from '@/lib/ap';
export async function POST(req: Request, { params }: { params: Promise<{ username: string }> }) {
... ...
if (body.type === 'Delete') {
console.log(`π Deleteλ κ²μ¦ μ€ν΅! / Skip validation for Delete! ${actorIdForVerify}`);
}
else if (!actorIdForVerify) {
console.error('β actor μμ! κ²μ¦ μ€ν΅!/ No actor! Skip verification!', body);
} else {
... ...
const verifyReq = new Request(req.url, {
method: req.method,
headers: req.headers,
body: rawBody
});
... ...
// βοΈ fetchActorPublicKeyWithRetryμ ν¬ν¨λ¨ / Included in fetchActorPublicKeyWithRetry
// const isValid = await verifyHttpSignature(verifyReq, publicKeyPem);
// β
myapp32 - μΊμ± + μ¬μλ! / Caching + Retry!
const { isValid } = await fetchActorPublicKeyWithRetry(
actorIdForVerify,
username,
verifyReq
);
... ...
}
}
π Outbox Public Key κ²μ¦
Outbox Public Key Verification
βοΈ Outboxλ μΊμλ₯Ό μ μ©νμ§ μμ΅λλ€.
The Outbox does not use caching.
π‘ inboxλ λ΄ μλ²μ κΈμ΄ λ€μ΄μ¬ λ μλλ°© μλ²κ° λ³ΈμΈμ private keyλ‘ μλͺ
ν΄μ 보λ΄λ©΄ λ΄ μλ²κ° κΈ λ³΄λ΄λ μλ²μ public keyλ₯Ό μμ²ν©λλ€. λ ν€κ° λ§μΌλ©΄ κΈ λ°κΈ°λ₯Ό νμ©ν©λλ€.
With the inbox mechanism, when a message arrives at my serverβsigned by the sender's server using its private keyβmy server requests the public key from the sending server. If the keys match, the message is accepted.
π‘ μΊμλ₯Ό μ μ©νλ©΄ λ΄ μλ²μ μλλ°© μλ²μ publick keyκ° μ μ₯λμ΄ μμΌλ―λ‘ μλλ°© μλ²μ public key μμ² νμκ° μ€μ΄λλλ€. κ·Έλμ κΈ λ°κΈ° μλκ° ν₯μλ©λλ€.
Implementing caching stores the other server's public key on your server, thereby reducing the number of requests made to the other server for that key. This results in faster content retrieval speeds.
π‘ outboxμμ POSTλ λ°λλ‘ λ΄κ° μμ±ν κΈμ μλλ°© μλ²μ 보λ΄λ κΈ°λ₯μ μνν©λλ€.
In the outbox, the POST method performs the function of sending a message I have written to the recipient's server.
π‘ κΈμ 보λ΄λ건 λ΄ μλ²μ μλμ κ΄λ ¨ μμΌλ μΊμλ₯Ό μ μ©νμ§ μμ΅λλ€.
Sending the content is unrelated to my server's speed, so caching is not applied.
βοΈ POST ν¨μμΆκ° / Add POST function
— myapp32/app/users/[username]/outbox/route.ts
// β
POST μΆκ°! Pinaforeκ° μ¬κΈ°λ‘ κΈ μ! / Added POST! Pinafore writes here!
export async function POST(req: Request, { params }: { params: Promise<{ username: string }> }) { ... }
βοΈ GETν¨μμ username νν° μΆκ°
Add a username filter to the GET function.
export async function GET(_req: Request, { params }: { params: Promise<{ username: string }> }) {
const { username } = await params;
// β
myap32 - username νν° μΆκ° / Added username filter
const posts = db.prepare('SELECT * FROM posts WHERE username = ? ORDER BY created_at DESC LIMIT 20').all(username) as any[];
try {
} catch (e) { }
π ν μ€νΈ / Test
βοΈ /api/posts λΌμ°ν°λ λ΄ UIμ μ© κΈμ°κΈ° λΌμ°νΈ
The /api/posts router is the route dedicated to post creation for my UI.
— κΈμ μμ±ν λ μλμ λΌμ°νΈλ‘ μ¬μ©ν©λλ€.
Use the route below when writing a post.
curl -X POST https://aloy-horizon.duckdns.org/api/posts \
-H "Content-Type: application/json" \
-d '{"username":"user1","content":"Hello local UI #apiposts"}'
βοΈ /users/[username]/outbox λΌμ°ν°λ activityPub νμ€λΌμ°νΈλ‘ κΈ μμ± ν λ μ¬μ©ν©λλ€.
The /users/[username]/outbox route handles post creation in accordance with the ActivityPub standard.
— pinaforeλ±μμ κΈ μμ± ν λ μ¬μ©ν¨.
Used when writing posts on platforms like Pinafore.
curl -X POST https://aloy-horizon.duckdns.org/users/user1/outbox \
-H "Content-Type: application/activity+json" \
-d '{"type":"Create","object":{"content":"Hello Activity UI #activityposts "}}'
βοΈ λ΄κ° μμ±ν κΈ λͺ©λ‘
List of posts I’ve written
https://aloy-horizon.duckdns.org/users/user1/outbox
βοΈ ν°λ―Έλ ν μ€νΈ / Terminal test
— λ‘컬 UI,pinaforeμμ κΈ μμ± ν λ μ¬μ©ν λΌμ°νΈ
Route to be used when composing a post in the local UI, Pinafore.
# api/posts POST ν°λ―Έλμμ μ€ν (λ‘컬 UI μ μ©)
# Execute api/posts POST request in the terminal (local UI only)
% curl -X POST https://aloy-horizon.duckdns.org/api/posts \
-H "Content-Type: application/json" \
-d '{"username":"user1","content":"Hello local UI #apiposts"}'
# μλ΅ / response
{"id":"aefc7ff8-5b38-4fb7-829d-9b14343cec6a","content":"Hello local UI #apiposts","created_at":"2026-09-05 03:39:37","username":"user1"}%
%
%
# users/[username]/outbox POST ν°λ―Έλμμ μ€ν(ActivityPub μ μ©)
# Execute POST to users/[username]/outbox in the terminal (ActivityPub only)
% curl -X POST https://aloy-horizon.duckdns.org/users/user1/outbox \
-H "Content-Type: application/activity+json" \
-d '{"type":"Create","object":{"content":"Hello Activity UI #activityposts"}}'
# μλ΅ / response
{"@context":"https://www.w3.org/ns/activitystreams","id":"https://aloy-horizon.duckdns.org/users/user1/posts/4ace7a75-3a40-494f-8dea-bf3f688b6cfb#activity","type":"Create","actor":"https://aloy-horizon.duckdns.org/users/user1","object":{"id":"https://aloy-horizon.duckdns.org/users/user1/posts/4ace7a75-3a40-494f-8dea-bf3f688b6cfb","type":"Note","attributedTo":"https://aloy-horizon.duckdns.org/users/user1","content":"Hello Activity UI #activityposts","published":"2026-09-05T03:39:48.980Z","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://aloy-horizon.duckdns.org/users/user1/followers"]}}
%
βοΈ λ§μ€νΈλμ κΈ λμ°© νμΈ
Confirmed receipt of post on Mastodon.

βοΈ gotosocialμ κΈ λμ°© νμΈ
Confirming receipt of post on GoToSocial

βοΈ μλ² λ‘κ·Έ / Server Log

μν볡μ 8μ₯ 32μ / John 8:32
“κ·Έλ¦¬κ³ λν¬λ μ§λ¦¬λ₯Ό μκ² λ κ²μ΄λ©°, μ§λ¦¬κ° λν¬λ₯Ό μμ λ‘κ² ν κ²μ΄λ€.”
“Then you will know the truth ,and the truth will set you free”