Social Media Quick Reference - nself-org/nchat GitHub Wiki
Fast reference guide for developers working with social media integration.
# Setup
./scripts/setup-social-media.sh
# Add credentials to .env.local
TWITTER_CLIENT_ID=xxx
TWITTER_CLIENT_SECRET=xxx
INSTAGRAM_APP_ID=xxx
INSTAGRAM_APP_SECRET=xxx
LINKEDIN_CLIENT_ID=xxx
LINKEDIN_CLIENT_SECRET=xxx
SOCIAL_MEDIA_ENCRYPTION_KEY=xxx
# Start server
pnpm dev
# Navigate to admin
http://localhost:3000/admin/social| What | Where |
|---|---|
| Migration | .backend/migrations/012_social_media_integration.sql |
| API Clients | src/lib/social/*-client.ts |
| API Routes | src/app/api/social/** |
| React Hooks | src/hooks/use-social-*.ts |
| Components | src/components/admin/Social*.tsx |
| GraphQL | src/graphql/social-media.ts |
| Docs | docs/Social-Media-Integration.md |
// Frontend
import { useSocialAccounts } from '@/hooks/use-social-accounts'
const { connectAccount } = useSocialAccounts()
connectAccount('twitter') // Redirects to OAuthimport { useSocialIntegrations } from '@/hooks/use-social-integrations'
import { useAuth } from '@/contexts/auth-context'
const { createIntegration } = useSocialIntegrations()
const { user } = useAuth()
await createIntegration({
accountId: 'uuid',
channelId: 'uuid',
autoPost: true,
filterHashtags: ['news', 'updates'],
filterKeywords: ['launch'],
minEngagement: 10,
createdBy: user.id,
})// From component
const { triggerImport } = useSocialAccounts()
const result = await triggerImport(accountId)
// From API
fetch('/api/social/poll', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accountId: 'optional' }),
})import { pollAllAccounts } from '@/lib/social/poller'
import { ApolloClient } from '@apollo/client'
const result = await pollAllAccounts(apolloClient)
console.log(result)
// { fetched: 10, imported: 8, filtered: 2, posted: 5, errors: [] }-- List all accounts
SELECT * FROM nchat_social_accounts;
-- List integrations with channel names
SELECT
i.*,
c.name as channel_name,
a.account_name
FROM nchat_social_integrations i
JOIN nchat_channels c ON i.channel_id = c.id
JOIN nchat_social_accounts a ON i.account_id = a.id;
-- Recent imports
SELECT * FROM nchat_social_posts
ORDER BY imported_at DESC
LIMIT 10;
-- Import logs
SELECT * FROM nchat_social_import_logs
ORDER BY started_at DESC
LIMIT 10;
-- Posts not yet posted to channels
SELECT * FROM nchat_social_posts
WHERE was_posted_to_channel = false;# OAuth flows
GET /api/social/{platform}/auth
GET /api/social/{platform}/callback
# Account management
GET /api/social/accounts
POST /api/social/accounts
DELETE /api/social/accounts?id={uuid}
# Polling
POST /api/social/poll # Poll all accounts
POST /api/social/poll {"accountId":"..."} # Poll specific account
GET /api/social/poll # Health check# Required
TWITTER_CLIENT_ID=
TWITTER_CLIENT_SECRET=
INSTAGRAM_APP_ID=
INSTAGRAM_APP_SECRET=
LINKEDIN_CLIENT_ID=
LINKEDIN_CLIENT_SECRET=
SOCIAL_MEDIA_ENCRYPTION_KEY=
NEXT_PUBLIC_APP_URL=
HASURA_ADMIN_SECRET=
# Optional
TWITTER_BEARER_TOKEN=# Edit crontab
crontab -e
# Add (polls every 5 minutes)
*/5 * * * * curl -X POST http://localhost:3000/api/social/poll{
"crons": [
{
"path": "/api/social/poll",
"schedule": "*/5 * * * *"
}
]
}functions:
socialPoll:
handler: handler.poll
events:
- schedule: rate(5 minutes)import type {
SocialAccount,
SocialPost,
SocialIntegration,
SocialPlatform,
} from '@/lib/social/types'
const platform: SocialPlatform = 'twitter' | 'instagram' | 'linkedin'import { matchesFilters } from '@/lib/social/filters'
const matches = matchesFilters(post, integration)
// Returns true if post passes all filters
// Filter criteria:
// - Hashtags: OR logic (any match)
// - Keywords: OR logic (any match)
// - Min engagement: threshold
// - Exclude retweets: boolean
// - Exclude replies: booleanimport { createSocialEmbed, formatAsMessageContent } from '@/lib/social/embed-formatter'
const embed = createSocialEmbed(post, 'twitter')
const messageContent = formatAsMessageContent(embed)
// Post to channel
await client.mutate({
mutation: POST_TO_CHANNEL,
variables: {
message: {
channel_id: channelId,
content: JSON.stringify(messageContent),
type: 'social_embed',
},
},
})import { encryptToken, decryptToken } from '@/lib/social/encryption'
// Encrypt before storage
const encrypted = encryptToken(accessToken)
// Decrypt for API calls
const accessToken = decryptToken(encrypted)- API Version: v2
- Cost: $100/month (Essential)
- Rate Limit: 300 req/15min
- Token Life: 2 hours (refresh available)
- OAuth: PKCE flow
import { TwitterClient } from '@/lib/social/twitter-client'
const client = new TwitterClient()- API: Graph API
- Cost: Free
- Rate Limit: 200 req/hour
- Token Life: 60 days (long-lived)
- Requirements: Business Account + Facebook Page
import { InstagramClient } from '@/lib/social/instagram-client'
const client = new InstagramClient()- API Version: v2
- Cost: Free
- Rate Limit: Varies
- Token Life: 60 days (no refresh)
- Requirements: Marketing Developer Platform access
import { LinkedInClient } from '@/lib/social/linkedin-client'
const client = new LinkedInClient()SELECT
a.account_name,
l.import_type,
l.posts_fetched,
l.posts_imported,
l.posts_filtered,
l.posts_posted,
l.errors,
l.status,
l.started_at,
l.completed_at
FROM nchat_social_import_logs l
JOIN nchat_social_accounts a ON l.account_id = a.id
ORDER BY l.started_at DESC
LIMIT 10;- Add
console.login callback route - Check state parameter matches
- Verify code is received
- Test token exchange
- Check encryption/decryption
import { matchesFilters } from '@/lib/social/filters'
const testPost = {
content: 'Check out our new #product launch!',
hashtags: ['product'],
engagement: { likes: 50 },
}
const testIntegration = {
filter_hashtags: ['product'],
min_engagement: 10,
}
console.log(matchesFilters(testPost, testIntegration)) // true| Issue | Solution |
|---|---|
| OAuth redirect error | Check NEXT_PUBLIC_APP_URL matches registered callback |
| No posts imported | Check filters, verify account has recent posts |
| Token expired | Implement refresh logic or re-authenticate |
| Rate limit hit | Reduce polling frequency or implement backoff |
| Posts not appearing in channel | Check integration auto_post is true |
- Poll Less Frequently: Change from 5min to 10min for lower load
-
Limit Posts per Poll: Use
limitparameter in API calls - Cache Account Data: Store in memory for duration of poll
-
Batch Database Inserts: Use
insert_manyinstead of individual inserts - Index Hashtags: Already done in migration
- Tokens encrypted at rest
- OAuth state validation
- Secure cookies (httpOnly, sameSite)
- RLS policies active
- Admin-only access
- Input validation
- SQL injection prevention
- No secrets in client code
# Test OAuth (Twitter)
curl http://localhost:3000/api/social/twitter/auth
# Test manual import
curl -X POST http://localhost:3000/api/social/poll \
-H "Content-Type: application/json" \
-d '{"accountId":"uuid-here"}'
# Test health check
curl http://localhost:3000/api/social/poll
# Test account creation (server-side only)
curl -X POST http://localhost:3000/api/social/accounts \
-H "Content-Type: application/json" \
-d '{
"platform": "twitter",
"account_id": "123",
"account_name": "Test",
"access_token_encrypted": "..."
}'Last Updated: January 30, 2026