{"id":7931,"date":"2026-08-14T13:35:12","date_gmt":"2026-08-14T04:35:12","guid":{"rendered":"https:\/\/www.freelifemakers.org\/wordpress\/?p=7931"},"modified":"2026-08-15T10:17:29","modified_gmt":"2026-08-15T01:17:29","slug":"nextjs-sns-server-3","status":"publish","type":"post","link":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/2026\/08\/14\/nextjs-sns-server-3\/","title":{"rendered":"[nextjs]SNS Server-3"},"content":{"rendered":"\n<p>\ud83d\udc49\ud83c\udffb public key\uac80\uc99d \ubd80\ubd84\uc744 \ucd94\uac00\ud588\uc2b5\ub2c8\ub2e4.<br>I have added the public key verification logic.<\/p>\n\n\n\n<p>\ud83d\udc49\ud83c\udffb \uae00\uc744 \ubc30\ub2ec\ud558\ub294 \uae30\ub2a5\uc774 \ucd94\uac00\ub418\uc5c8\uc2b5\ub2c8\ub2e4.<br>A feature for delivering posts has been added.<\/p>\n\n\n\n<p>\ud83d\udc49\ud83c\udffb \uc774\uc804 \ud3ec\uc2a4\ud2b8\uc5d0\uc11c freelifemakers.com\uc5d0\uc11c \ub0b4 \uc11c\ubc84(yourhost.domain.org)\ub97c \ud314\ub85c\uc6b0\ud558\uc600\uc2b5\ub2c8\ub2e4.<br>In the previous post, I followed my server (yourhost.domain.org) from freelifemakers.com.<\/p>\n\n\n\n<p>\ud83d\udc49\ud83c\udffb \ud314\ub85c\uc6b0 \ud560 \ub54c \uc800\uc7a5\ub41c \uc815\ubcf4\uc5d0 \ub530\ub77c \ub0b4 \uc11c\ubc84\uc5d0\uc11c \uae00\uc744 \uc4f0\uba74 freelifemakers.com\uc11c\ubc84\ub85c \uae00\uc744 \ubc30\ub2ec\ud569\ub2c8\ub2e4.<br>When you follow someone, posts written on your server are delivered to the freelifemakers.com server based on the stored information.<\/p>\n\n\n\n<p>\ud83d\udc49\ud83c\udffb pinafore.social\uc5d0\uc11c \ubcf4\ub294 \ubaa8\ub4e0 \uc815\ubcf4\ub294 freelifemakers.com\uc11c\ubc84\uc5d0 \uc800\uc7a5\ub41c \uc815\ubcf4\ub97c \ubcf4\uac8c\ub429\ub2c8\ub2e4.<br>All information viewed on pinafore.social is retrieved from data stored on the freelifemakers.com server.<\/p>\n\n\n\n<p>\ud83d\udcc1 \uc804\uccb4 \ud504\ub85c\uc81d\ud2b8 \uad6c\uc870 \/ Project Structure<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>myapp12\/  \n\u251c\u2500\u2500 app\/  (Next.js App Router)\n\u2502   \u251c\u2500\u2500 api\/posts\/route.ts     -&gt; \uae00 \uc4f0\uae30 API \/ Writing API\n\u2502   \u251c\u2500\u2500 users\/&#91;username]\/\n\u2502   \u2502   \u251c\u2500\u2500 route.ts           -&gt; Actor\uc815\ubcf4 \/ Acotr Information\n\u2502   \u2502   \u251c\u2500\u2500 inbox\/route.ts     -&gt; Inbox\n\u2502   \u2502   \u2514\u2500\u2500 outbox\/route.ts    -&gt; outbox\n\u2502   \u251c\u2500\u2500 layout.tsx, page.tsx, globals.css\n\u2502   \u2514\u2500\u2500 favicon.ico\n\u251c\u2500\u2500 lib\/\n\u2502   \u251c\u2500\u2500 ap.ts                  -&gt; Follow Accept\n\u2502   \u2514\u2500\u2500 db.ts                  -&gt; DB connection\n\u251c\u2500\u2500 data\/\n\u2502   \u2514\u2500\u2500 keys\/                  -&gt; private.pem, public.pem\n\u251c\u2500\u2500 data.sqlite                -&gt; Database\n\u251c\u2500\u2500 Caddyfile                  -&gt; https \n\u2514\u2500\u2500 package.json<\/code><\/pre>\n\n\n\n<p>\ud83d\udcc1 \ud504\ub85c\uc81d\ud2b8 \uc2dc\uc791(myapp13)<br>Project Start (myapp13)<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npx create-next-app@latest<\/code><\/pre>\n\n\n\n<p>\ud83d\udcc1 SQLite\uc124\uce58 \/ Installing SQLite<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cd ~\/myapp13\nnpm install better-sqlite3\nnpm install -D @types\/better-sqlite3<\/code><\/pre>\n\n\n\n<p>\ud83d\udcc1 \ub3c4\uba54\uc778 \ud5c8\uc6a9 \/ Allow Domain<\/p>\n\n\n\n<p>&#8212; next.config.ts\uc5d0 \uc544\ub798\ucc98\ub7fc \ub3c4\uba54\uc778\uc744 \ud5c8\uc6a9\ud569\ub2c8\ub2e4.<br>Allow the domain in <code>next.config.ts<\/code> as shown below.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {\n  \/* config options here *\/\nallowedDevOrigins: &#91;<strong>'host.yourdomain.org', '*.yourdomain.org<\/strong>'],\n};\n\nexport default nextConfig;<\/code><\/pre>\n\n\n\n<p>\ud83d\udcc1 https\uc124\uc815 \/ HTTPS configuration<\/p>\n\n\n\n<p>\u2714\ufe0f Caddy \uc124\uce58(MacOS) \/ Installing Caddy (macOS)<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>brew install caddy<\/code><\/pre>\n\n\n\n<p>\u2714\ufe0f Caddy file \ub9cc\ub4e4\uae30(myapp11 \ud504\ub85c\uc81d\ud2b8\ub0b4 )<br>Create a Caddyfile (within the myapp11 project)<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># \ud30c\uc77c\uc5f4\uae30 \/ Open File\nnano Caddyfile\n\n# \uc124\uc815 \uc791\uc131 \/ Create Configuration\nyourhost.domain.org {\n    reverse_proxy localhost:3000\n}\n\n#\ud30c\uc77c \uc800\uc815\ud6c4 \uc885\ub8cc \/ Save file and exit (Ctrl + O,Ctrl + x)<\/code><\/pre>\n\n\n\n<p>\u2714\ufe0f Caddy \uc2e4\ud589 \/ Run Caddy<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>caddy fmt --overwrite Caddyfile\ncaddy run\n\n# background\ncaddy start\ncaddy stop<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>\ud83d\udcc1 \ucf54\ub4dc \uc791\uc131 \/ Writing code<\/p>\n\n\n\n<p>\u2714\ufe0f myapp13\/app\/users\/[username]\/route.ts<\/p>\n\n\n\n<p>&#8212; public key \uac80\uc99d\uae30\ub2a5 \ubd80\ubd84\uc785\ub2c8\ub2e4.<br>This is the public key verification function.<\/p>\n\n\n\n<p>&#8212; publick key \uac80\uc99d \uc791\ub3d9\ud655\uc778 \ud560 \uc218 \uc788\uace0 public key\uac00 \uc778\uc99d\ub418\uc9c0 \uc54a\uc544\ub3c4 \uc624\ub958\ub97c \ubc1c\uc0dd\uc2dc\ud0a4\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.<br>You can verify that public key validation is working, and it does not trigger an error even if the public key is not authenticated.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>    \/\/ 1. Undo\ub294 \uc81c\uc77c \uba3c\uc800! \uac80\uc99d \uc5c6\uc774 \ucc98\ub9ac\ud574\uc57c \uc5b8\ud314\ub85c\uc6b0\uac00 \ub428 \/ Undo should be processed first without verification to allow unfollowing\n    if (body.type === 'Undo') {\n      const targetActor = typeof body.object?.actor === 'string' ? body.object.actor : body.object?.actor?.id || body.object?.id || body.actor;\n      \n       \/\/ Follow Undo\uc778 \uacbd\uc6b0 \/ If it's a Follow Undo\n      if (body.object?.type === 'Follow' || typeof body.object === 'string' || body.object?.id?.includes('#follow')) {\n         const unfollowActorId = body.actor; \/\/ \ub204\uac00 \uc5b8\ud314\ud588\ub294\uc9c0 \/ who unfollowed\n         db.prepare('DELETE FROM followers WHERE actor = ?').run(unfollowActorId);\n         console.log(`\ud83d\uddd1\ufe0f &#91;${username}] \uc5b8\ud314\ub85c\uc6b0 \/ unfollow : ${unfollowActorId}`);\n      }\n      return new Response('', { status: 202 });\n    }\n\n    \/\/ 2. \uac80\uc99d - \ud14c\uc2a4\ud2b8\ub77c \uc2a4\ud0b5, \uadfc\ub370 publicKeyPem \uc5c6\uc744\ub54c \ud130\uc9c0\uc9c0 \uc54a\uac8c \ubc29\uc5b4 \/ Verification - skipped for testing, but defend against missing publicKeyPem\n    try {\n      const actorUrl = body.actor;\n      const actorData = await fetch(actorUrl, {\n        headers: { Accept: 'application\/activity+json' }\n      }).then(r =&gt; r.json());\n      \n      \/\/ optional chaining\uc73c\ub85c \ubc29\uc5b4 \/ Defend with optional chaining\n      const publicKeyPem = actorData?.publicKey?.publicKeyPem;\n      \n      if (!publicKeyPem) {\n        console.log(`\u26a0\ufe0f &#91;${username}] publicKey \uc5c6\uc74c, \uac80\uc99d \uc2a4\ud0b5 \/ no publicKey, skip verify`);\n      } else {\n        \/\/ const isValid = verify(req, publicKeyPem); \n        \/\/ if (!isValid) return Response.json({}, { status: 401 });\n      }\n    } catch (verErr) {\n      console.log(`\u26a0\ufe0f &#91;${username}] actor fetch \uc2e4\ud328, \uac80\uc99d \uc2a4\ud0b5 \/ fetch failed, skip verify`, verErr);\n    }<\/code><\/pre>\n\n\n\n<p>\u2714\ufe0f myapp13\/app\/api\/posts\/route.ts<\/p>\n\n\n\n<p>&#8212; \uae00 \ubc30\ub2ec\ud558\ub294 \uae30\ub2a5\uc785\ub2c8\ub2e4.<br>This is a feature for delivering posts.<\/p>\n\n\n\n<p>&#8212; \uae30\uc874\ucf54\ub4dc\uc5d0\uc11c POST\ud568\uc218 \ubd80\ubd84\ub9cc \uc218\uc815\ub418\uc5c8\uc2b5\ub2c8\ub2e4.<br>Only the POST function section of the existing code has been modified.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ myapp13 \u2705\nexport async function POST(req: Request) {\n  const { content, username = 'user1' } = await req.json();\n  if (!content) return Response.json({ error: '\ub0b4\uc6a9 \uc5c6\uc74c \/ Content is required' }, { status: 400 });\n\n  const id = randomUUID();\n  db.prepare('INSERT INTO posts (id, content) VALUES (?, ?)').run(id, content);\n\n    \/\/ 1. ActivityPub Note \ub9cc\ub4e4\uae30 \/ Create ActivityPub Note\n  const noteId = `https:\/\/aloy-horizon.duckdns.org\/users\/${username}\/posts\/${id}`;\n  const note = {\n    id: noteId,\n    type: 'Note',\n    attributedTo: `https:\/\/aloy-horizon.duckdns.org\/users\/${username}`,\n    content: content,\n    to: &#91;'https:\/\/www.w3.org\/ns\/activitystreams#Public'],\n    cc: &#91;`https:\/\/aloy-horizon.duckdns.org\/users\/${username}\/followers`]\n  };\n\n  \/\/ 2. \ud314\ub85c\uc6cc\ub4e4\ud55c\ud14c \ubc30\ub2ec! \/ Deliver to followers!\n  const followers = db.prepare('SELECT * FROM followers').all() as any&#91;];\n  console.log(`\ud83d\udce4 ${followers.length}\uba85\uc5d0\uac8c \ubc30\ub2ec \uc2dc\uc791 \/ delivering to ${followers.length} followers`);\n\n  for (const follower of followers) {\n    try {\n      await sendNote(follower.inbox, note, username, id, content);\n      console.log(`\u2705 \ubc30\ub2ec \uc131\uacf5 \/ Delivery successful -&gt; ${follower.actor}`);\n    } catch (e) {\n      console.error(`\u274c \ubc30\ub2ec \uc2e4\ud328 \/ Delivery failed -&gt; ${follower.actor}`, e);\n    }\n  }\n\n  const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(id);\n  return Response.json(post);\n}<\/code><\/pre>\n\n\n\n<p>\u2714\ufe0f myapp13\/lib\/ap.ts<\/p>\n\n\n\n<p>&#8212; \uae30\uc874 \ucf54\ub4dc\uc5d0\uc11c sendNote\ud568\uc218\uac00 \ucd94\uac00\ub418\uc5c8\uc2b5\ub2c8\ub2e4.<br>The <code>sendNote<\/code> function has been added to the existing code.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export async function sendNote(toInbox: string, note: any, username: string, postId: string, content: string) {\n  const actorId = `https:\/\/${DOMAIN}\/users\/${username}`;\n  \n  const createDoc = {\n    '@context': 'https:\/\/www.w3.org\/ns\/activitystreams',\n    id: `${actorId}\/posts\/${postId}#create`,\n    type: 'Create',\n    actor: actorId,\n    object: note\n  };\n\n  const body = JSON.stringify(createDoc);\n  const url = new URL(toInbox);\n  \n  const digest = `SHA-256=${crypto.createHash('sha256').update(body).digest('base64')}`;\n  const date = new Date().toUTCString();\n  const signingString = `(request-target): post ${url.pathname}\\nhost: ${url.host}\\ndate: ${date}\\ndigest: ${digest}`;\n  \n  const signer = crypto.createSign('sha256');\n  signer.update(signingString);\n  const signature = signer.sign(PRIVATE_KEY, 'base64');\n\n  const keyId = `${actorId}#main-key`;\n  const sigHeader = `keyId=\"${keyId}\",headers=\"(request-target) host date digest\",signature=\"${signature}\"`;\n\n  console.log(`\ud83d\udcdd &#91;${username}] Note \uc804\uc1a1 \/ Note sent -&gt; ${toInbox} : ${content.slice(0,20)}`);\n\n  const res = await fetch(toInbox, {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application\/activity+json',\n      'Date': date,\n      'Digest': digest,\n      'Signature': sigHeader,\n      'Host': url.host\n    },\n    body\n  });\n\n  const text = await res.text();\n  console.log(`\ud83d\udcec Note \uacb0\uacfc \/ Note result: ${res.status}`, text);\n  return res.ok;\n}<\/code><\/pre>\n\n\n\n<p>\ud83d\udcc1 \uae00 \ubc30\ub2ec \ud14c\uc2a4\ud2b8 \/ Text Delivery Test<\/p>\n\n\n\n<p>\u2714\ufe0f \uae00\uc4f0\uae30(\ud130\ubbf8\ub110\uc5d0\uc11c \uc2e4\ud589) \/ Writing (run in the terminal)<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># 1. \uae00\uc4f0\uae30(localhost \ub9d0\uace0 https \ub3c4\uba54\uc778\uc73c\ub85c \ubcf4\ub0b4\uae30)\nWriting (Send via HTTPS domain instead of localhost)\n\ncurl -X POST https:\/\/aloy-horizon.duckdns.org\/api\/posts \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\"content\":\"Hello! my SNS!!\", \"username\":\"user1\"}'\n\n# 2. outbox\uc5d0 \ub728\ub294\uc9c0 \ud655\uc778!\nCheck if it appears in the outbox!\n\ncurl https:\/\/aloy-horizon.duckdns.org\/users\/user1\/outbox \\\n  -H \"Accept: application\/activity+json\" | jq\n\n# 3. \ud314\ub85c\uc6cc \uc788\ub294\uc9c0 \ud655\uc778!\nCheck if you have any followers!\n\nsqlite3 data.sqlite \"SELECT * FROM followers;\"<\/code><\/pre>\n\n\n\n<p>\u2714\ufe0f \uae00 \ubc30\ub2ec\uc774 \ub418\ub294\uc9c0 \ud655\uc778 \/ Check if the message is being delivered.<\/p>\n\n\n\n<p>&#8212; freelifemakers.com\uc5d0\uc11c \ub0b4 \uc11c\ubc84\uc5d0 \ud314\ub85c\uc6b0 \ub418\uc5b4 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4.<br>You must be following my server on freelifemakers.com.<\/p>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\ud83d\udc49\ud83c\udffb public key\uac80\uc99d \ubd80\ubd84\uc744 \ucd94\uac00\ud588\uc2b5\ub2c8\ub2e4.I have added the public key verification logic. \ud83d\udc49\ud83c\udffb \uae00\uc744 \ubc30\ub2ec\ud558\ub294 \uae30\ub2a5\uc774 \ucd94\uac00\ub418\uc5c8\uc2b5\ub2c8\ub2e4.A feature for delivering posts has been added. \ud83d\udc49\ud83c\udffb \uc774\uc804 \ud3ec\uc2a4\ud2b8\uc5d0\uc11c freelifemakers.com\uc5d0\uc11c \ub0b4 \uc11c\ubc84(yourhost.domain.org)\ub97c \ud314\ub85c\uc6b0\ud558\uc600\uc2b5\ub2c8\ub2e4.In the previous post, I followed my server (yourhost.domain.org) from freelifemakers.com. \ud83d\udc49\ud83c\udffb \ud314\ub85c\uc6b0 \ud560 \ub54c \uc800\uc7a5\ub41c \uc815\ubcf4\uc5d0 \ub530\ub77c \ub0b4 \uc11c\ubc84\uc5d0\uc11c \uae00\uc744 \uc4f0\uba74 freelifemakers.com\uc11c\ubc84\ub85c \uae00\uc744 \ubc30\ub2ec\ud569\ub2c8\ub2e4.When [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[27,1,7],"tags":[],"class_list":["post-7931","post","type-post","status-publish","format-standard","hentry","category-nextjs","category-uncategorized","category-website","missing-thumbnail"],"_links":{"self":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/7931","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/comments?post=7931"}],"version-history":[{"count":49,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/7931\/revisions"}],"predecessor-version":[{"id":7985,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/7931\/revisions\/7985"}],"wp:attachment":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/media?parent=7931"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/categories?post=7931"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/tags?post=7931"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}