- Email must be unique and verified before login is allowed
- Password must be minimum 8 characters
- Verification email sent within 60 seconds
Instagram Publishing Module
Technical deep-dive into the Instagram Graph API integration, OAuth flows, webhook handling, and queue-based publishing engine for the SMS platform.
Functional Requirements
Each requirement includes Given/When/Then acceptance criteria, priority, and owner. These form the basis for QA test cases and developer task breakdown.
Authentication
- Given a new user submits the registration form with a unique email and 8+ char password, When they click Register, Then account is created, verification email is sent, and they are redirected to the verify-email screen.
- Given a duplicate email is submitted, When registration is attempted, Then a validation error "Email already registered" is shown.
Instagram Accounts
- Given user clicks "Connect Instagram", When they complete the Meta OAuth flow and grant permissions, Then their account is saved, access token is encrypted at rest, and they see it in the accounts list.
- Given the user denies permissions, When redirected back, Then an error message "Permission denied. Please allow all required permissions." is shown.
Media Library
- Given user uploads a JPG under 8MB, When the upload completes, Then thumbnail is generated, record is saved to DB, and the file appears in the media grid immediately.
- Given user uploads a file over size limit, When file is selected, Then the upload is rejected client-side with message "File too large. Max 8MB for images, 100MB for video."
Caption Library
AI Assistant
Scheduler
Calendar
Publishing Engine
Notifications & Analytics
Non-Functional Requirements
| Category | Requirement | Target |
|---|---|---|
| Performance | Page Load Time | < 2 seconds (LCP) |
| Performance | API Response Time | < 300ms (p95) |
| Performance | Calendar Render | < 1 second |
| Performance | Queue Processing | < 60 seconds per job |
| Availability | System Uptime | 99.9% (SLA) |
| Scalability | Concurrent Users | 10,000 active users |
| Scalability | Scheduled Posts | Millions of records |
| Reliability | Duplicate Job Prevention | Idempotent job IDs |
| Security | Password Hashing | bcrypt, cost factor 12 |
| Security | Token Storage | Encrypted at rest (AES-256) |
| Security | HTTPS | TLS 1.2+ required |
| Security | Rate Limiting | 100 req/min per user |
| Accessibility | Standard | WCAG 2.1 AA |
| Browser Support | Target Browsers | Chrome, Firefox, Edge, Safari (last 2 versions) |
| Compliance | Data Privacy | No third-party tracking, local storage only |
User Stories
- OAuth flow completes within Instagram's popup
- Account appears in account list within 5 seconds
- Token stored securely and encrypted
- AI generates caption within 5 seconds
- Caption respects Instagram 2200 char limit
- User can edit before saving
- AI error shows fallback to manual editor
- Cannot schedule in past time
- Post enters queue and shows Scheduled status
- Confirmation screen shows exact publish time in account timezone
- Cannot drag to past time
- Confirm modal appears before saving
- Post status updated instantly on calendar
- System retries 3 times with exponential backoff
- User notified after final failure
- Retry count visible in post detail
- Shows likes, comments, shares, saves, reach, impressions
- Data synced from Instagram within 24 hours of publishing
- Engagement rate calculated automatically
Business Rules
Timezone Rules
| Rule | Behavior |
|---|---|
| Storage | All publish timestamps stored in UTC in publish_at_utc column
|
| Display | All times displayed to user in account's configured IANA timezone |
| Drag & Drop | Rescheduling recalculates UTC automatically from new time + account timezone |
| DST | System uses IANA timezone library — DST transitions handled automatically |
| Timezone Change | If account timezone is changed after scheduling, display times update but UTC preserved |
Scheduling Rules
- Cannot schedule post in the past (validation at form and API level)
- Cannot schedule post if Instagram token is expired or account is disconnected
- Cannot edit a post within 5 minutes of its scheduled publish time
- Duplicate posts (same account, same time) trigger a warning — user must confirm
- Carousel posts require minimum 2 and maximum 10 images
- Video posts support only MP4 format, max 100MB, max 60 seconds duration
Publishing Rules
- Publishing worker runs every 60 seconds to check for due jobs
- Job is locked before processing to prevent duplicate publishing (idempotency key)
- Maximum 3 retry attempts with exponential backoff (1m, 5m, 15m)
- After max retries, job moves to Permanently Failed status and user is notified
- Published posts cannot be deleted — only archived for audit history
- Analytics sync runs 1 hour after successful publish
Media Rules
- Supported image formats: JPG, PNG, WEBP — max 8MB per file
- Supported video format: MP4 — max 100MB, max 60 seconds
- Media cannot be deleted if referenced by a scheduled or processing post
- Thumbnails generated automatically for all uploaded media
- File names sanitized and stored with UUID-based names (original name preserved in metadata)
Caption Rules
- Maximum caption length: 2200 characters (Instagram limit)
- Maximum hashtags: 30 per post (Instagram limit)
- Caption is optional for image posts, required for carousel posts
- AI-generated captions are logged in ai_requests table (prompt, response, token usage)
Validation Rules
User Registration Form
| Field | Rule | Error Message |
|---|---|---|
| name | Required, 2–100 chars, alpha + spaces only | "Name must be 2–100 characters" |
| Required, valid email format, unique in DB | "Email is already registered" / "Invalid email" | |
| password | Required, min 8 chars, 1 uppercase, 1 number | "Password must be 8+ chars with uppercase and number" |
| password_confirm | Must match password field | "Passwords do not match" |
Create Post Form
| Field | Rule | Error Message |
|---|---|---|
| instagram_account_id | Required, must belong to current user, token must be valid | "Please select a valid Instagram account" |
| media_id | Required, must belong to current user, file must exist on disk | "Selected media not found or deleted" |
| caption | Optional, max 2200 chars | "Caption cannot exceed 2200 characters" |
| publish_date | Required, must be today or future date | "Publish date cannot be in the past" |
| publish_time | Required, if today — must be at least 5 min in future | "Publish time must be at least 5 minutes from now" |
| timezone | Required, valid IANA timezone string | "Invalid timezone" |
Media Upload Validation
| Check | Rule |
|---|---|
| File Type | Whitelist: image/jpeg, image/png, image/webp, video/mp4 |
| Image Size | Max 8MB (8,388,608 bytes) |
| Video Size | Max 100MB (104,857,600 bytes) |
| Video Duration | Max 60 seconds (validated server-side via FFprobe) |
| Image Dimensions | Min 320x320px, Max 1440x1440px for Instagram compatibility |
| Virus Scan | ClamAV scan before storing (production only) |
API Request Validation
All API endpoints validate: Authorization header (Bearer JWT), Content-Type: application/json, and required fields presence before processing. Invalid requests return HTTP 422 with field-level error details.
Edge Cases
| Scenario | Trigger | Expected Behavior | Severity |
|---|---|---|---|
| Expired OAuth Token | Instagram token expires during publish | Cancel job, mark Failed, notify user, prompt reconnect | Critical |
| Account Revoked Externally | User removes app from Instagram settings | Mark account disconnected, suspend all scheduled posts, log event | Critical |
| Schedule in Past | User submits form with past date/time | Reject with validation error — "Publish time cannot be in the past" | High |
| Duplicate Schedule | Same account + same time + same media | Show warning modal — user confirms or cancels | High |
| Media Deleted Before Publish | User deletes media linked to scheduled post | Block deletion with error — "Media is used in X scheduled posts" | High |
| Instagram API Timeout | API does not respond within 30s | Retry with backoff, log timeout event | High |
| Instagram API Rate Limit | 429 response from Instagram | Delay job by rate limit reset window, retry automatically | High |
| Queue Worker Crash | Redis worker process dies mid-job | Job remains in processing state — watchdog detects and requeues after 5 min | Critical |
| AI Service Unavailable | OpenAI API returns 503 | Show friendly error, allow manual caption editing, log failure | Medium |
| Empty AI Response | OpenAI returns empty content | Inform user, offer retry option | Medium |
| Timezone Changed Post-Schedule | User changes account timezone | Recalculate display time — UTC publish_at_utc preserved unchanged | Medium |
| Drag to Invalid Slot | User drags post to past time or invalid slot | Snap back to original position, show brief error toast | Medium |
| Storage Full | Disk capacity exceeded | Reject uploads, alert admin, show user-friendly error | Critical |
| Race Condition on Publish | Two workers pick same job simultaneously | Idempotency lock prevents duplicate publishing — second worker skips | Critical |
| Session Expired Mid-Form | JWT expires while user is filling form | Preserve form data in localStorage, redirect to login, restore after re-auth | Medium |
| DST Clock Change | Daylight Saving Time transition | IANA timezone library handles automatically — no manual intervention needed | Medium |
Error Handling
| Code | HTTP | Reason | User Message | Resolution |
|---|---|---|---|---|
| E001 | 401 | Invalid credentials | "Email or password is incorrect." | User retries login; lockout after 5 attempts |
| E002 | 401 | Session expired | "Your session has expired. Please log in again." | Auto-redirect to login with return URL |
| E003 | 403 | Instagram token expired | "Instagram connection expired. Please reconnect your account." | Show reconnect button → OAuth flow |
| E004 | 404 | Media not found | "Selected media could not be found." | Prompt user to reselect media |
| E005 | 422 | Caption too long | "Caption exceeds 2200 character limit." | Real-time counter prevents submission |
| E006 | 422 | Past schedule time | "Publish time must be in the future." | Datepicker validation; API secondary check |
| E007 | 503 | AI service unavailable | "AI assistant is temporarily unavailable. Please write caption manually." | Retry after 30 seconds; fallback to manual |
| E008 | 500 | Queue failure | "Publishing system error. Our team has been notified." | Auto-retry; engineering alert via monitoring |
| E009 | 502 | Publishing failed | "Post could not be published. Retrying automatically." | Exponential backoff retry; user notified after final failure |
| E010 | 429 | Rate limit exceeded | "Too many requests. Please wait and try again." | Return Retry-After header; client respects backoff |
| E011 | 413 | File too large | "File size exceeds the limit (8MB for images, 100MB for videos)." | Client-side validation before upload |
| E012 | 415 | Unsupported media type | "Unsupported file format. Supported: JPG, PNG, WEBP, MP4." | File type check before upload; server rejects invalid types |
API Error Response Format
API Design
Base URL: /api/v1/ — All endpoints require
Authorization: Bearer {jwt_token} header. Content-Type:
application/json. Rate limit: 100 requests/minute per user.
Authentication Endpoints
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| POST | /auth/register | Register new user account | ❌ Public |
| POST | /auth/login | Login, returns JWT + refresh token | ❌ Public |
| POST | /auth/logout | Invalidate refresh token | ✅ JWT |
| POST | /auth/refresh | Get new access token via refresh token | ✅ Refresh |
| POST | /auth/password/reset | Send password reset email | ❌ Public |
Scheduler Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /scheduler/posts | Create scheduled post |
| GET | /scheduler/posts | List posts (params: account_id, status, date, search, page, limit) |
| GET | /scheduler/posts/{id} | Get post details with media, caption, queue status |
| PUT | /scheduler/posts/{id} | Update post (before publish window) |
| POST | /scheduler/posts/{id}/duplicate | Clone post as new draft |
| POST | /scheduler/posts/{id}/reschedule | Change publish date/time |
| POST | /scheduler/posts/{id}/cancel | Cancel scheduled post |
| DELETE | /scheduler/posts/{id} | Delete draft post |
| GET | /scheduler/calendar | Get calendar events (params: date, account_id) |
Create Post — Request & Response Example
Media Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /media/upload | Upload media file (multipart/form-data) |
| GET | /media | List media (params: search, folder_id, type, page) |
| GET | /media/{id} | Get media details |
| DELETE | /media/{id} | Delete media (soft delete) |
AI & Analytics Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /ai/caption/generate | Generate caption from prompt via OpenAI |
| POST | /ai/caption/rewrite | Rewrite existing caption |
| POST | /ai/hashtags/generate | Generate hashtag suggestions |
| GET | /analytics | Analytics dashboard data |
| GET | /analytics/posts/{id} | Per-post analytics metrics |
| GET | /notifications | List user notifications |
| PUT | /notifications/{id}/read | Mark notification as read |
Database Design
Database: MySQL 8.0. All tables use UUID primary keys, UTC timestamps, and soft deletes where applicable. Foreign key constraints enforced at DB level.
| Column | Type | Properties | Description |
|---|---|---|---|
| id | BIGINT UNSIGNED | PKAUTO_INC | Primary Key |
| uuid | CHAR(36) | NOT NULLUNIQUE | External Reference ID |
| name | VARCHAR(100) | NOT NULL | Full Name |
| VARCHAR(255) | NOT NULLUNIQUE | Login Email | |
| password | VARCHAR(255) | NOT NULL | bcrypt hash |
| avatar | VARCHAR(500) | NULL | Profile Image URL |
| status | ENUM | DEFAULT 'active' | 'active','inactive','banned' |
| last_login_at | TIMESTAMP | NULL | |
| email_verified_at | TIMESTAMP | NULL | |
| created_at | TIMESTAMP | DEFAULT NOW() | |
| updated_at | TIMESTAMP | ON UPDATE NOW() | |
| deleted_at | TIMESTAMP | NULL | soft delete |
| Column | Type | Properties | Description |
|---|---|---|---|
| id | BIGINT UNSIGNED | PKAUTO_INC | |
| uuid | CHAR(36) | NOT NULLUNIQUE | |
| user_id | BIGINT UNSIGNED | NOT NULLFK | REFERENCES users(id) ON DELETE CASCADE |
| instagram_user_id | VARCHAR(100) | NOT NULL | Meta Graph ID |
| username | VARCHAR(100) | NOT NULL | Instagram Handle |
| display_name | VARCHAR(255) | NULL | |
| profile_picture | VARCHAR(500) | NULL | |
| access_token | TEXT | NOT NULL | AES-256 encrypted |
| token_expires_at | TIMESTAMP | NULL | |
| timezone | VARCHAR(100) | DEFAULT 'UTC' | Account scheduling timezone |
| status | ENUM | DEFAULT 'active' | 'active','expired','disconnected' |
| publishing_enabled | TINYINT(1) | DEFAULT 1 | |
| created_at | TIMESTAMP | DEFAULT NOW() | |
| updated_at | TIMESTAMP | ON UPDATE NOW() |
| Column | Type | Properties | Description |
|---|---|---|---|
| id | BIGINT UNSIGNED | PKAUTO_INC | |
| uuid | CHAR(36) | NOT NULLUNIQUE | |
| user_id | BIGINT UNSIGNED | NOT NULLFK | REFERENCES users(id) |
| instagram_account_id | BIGINT UNSIGNED | NOT NULLFK | REFERENCES instagram_accounts(id) |
| media_id | BIGINT UNSIGNED | NULL | Foreign key to media table |
| caption_id | BIGINT UNSIGNED | NULL | Foreign key to captions table |
| title | VARCHAR(255) | NULL | Internal note/title |
| status | ENUM | DEFAULT 'draft' | 'draft','scheduled','queued','processing','published','failed','retrying','cancelled' |
| publish_date | DATE | NOT NULL | Local user date |
| publish_time | TIME | NOT NULL | Local user time |
| publish_at_utc | TIMESTAMP | NOT NULL | Converted absolute UTC datetime |
| published_at | TIMESTAMP | NULL | Actual publish time |
| retry_count | TINYINT | DEFAULT 0 | |
| error_message | TEXT | NULL | |
| created_at | TIMESTAMP | DEFAULT NOW() | |
| updated_at | TIMESTAMP | ON UPDATE NOW() | |
| deleted_at | TIMESTAMP | NULL |
| Column | Type | Properties | Description |
|---|---|---|---|
| id | BIGINT UNSIGNED | PKAUTO_INC | |
| uuid | CHAR(36) | NOT NULLUNIQUE | |
| scheduled_post_id | BIGINT UNSIGNED | NOT NULLFK | REFERENCES scheduled_posts(id) |
| idempotency_key | CHAR(64) | NOT NULLUNIQUE | Prevents duplicate execution |
| queue_name | VARCHAR(100) | DEFAULT 'publishing' | |
| status | ENUM | DEFAULT 'pending' | 'pending','processing','published','failed' |
| priority | TINYINT | DEFAULT 5 | |
| retry_count | TINYINT | DEFAULT 0 | |
| error_message | TEXT | NULL | |
| api_response | JSON | NULL | Raw payload from Meta API |
| scheduled_at | TIMESTAMP | NOT NULL | |
| started_at | TIMESTAMP | NULL | |
| finished_at | TIMESTAMP | NULL | |
| created_at | TIMESTAMP | DEFAULT NOW() | |
| updated_at | TIMESTAMP | ON UPDATE NOW() |
Other Tables Summary
Architecture
Architecture Style: Modular Monolith (MVP) — Service-oriented ready. API-first. Queue-based processing. Event-driven background jobs. Designed to split into microservices in Phase 3.
System Architecture Diagram
High-Level Architecture Flow
Publishing Sequence
- Validate all fields
- Convert timezone to UTC (
publish_at_utc) - Store in
scheduled_posts(status: scheduled) - Create
publish_jobsentry (status: pending)
- SELECT jobs WHERE scheduled_at <= NOW() AND status = 'pending'
- Lock job with
idempotency_key(UPDATE ... WHERE status = 'pending') - Validate: token valid, media exists, account active
- Upload media container
- Publish container to feed
- Store API response
- Update
scheduled_postsstatus = 'published' - Update
publish_jobsstatus = 'published' - Schedule analytics sync (1hr delay)
- Send success notification
- Log
error_message+api_response - If retry_count < 3: requeue with exponential backoff
- If retry_count >= 3: status = 'failed', notify user
Technology Stack
| Layer | Technology | Purpose |
|---|---|---|
| Frontend | React + TypeScript + Tailwind CSS | SPA, Calendar, Dashboard, Forms |
| Backend | PHP 8.2 + Laravel 11 | REST API, Business Logic, Queue Management |
| Database | MySQL 8.0 | All persistent data storage |
| Queue | Redis + Laravel Horizon | Background job processing, retry management |
| AI | OpenAI GPT-4o API | Caption generation, hashtag suggestions |
| Storage | Local FileSystem (Laravel Storage) | Media files, thumbnails |
| Auth | JWT (tymon/jwt-auth) | Stateless API authentication |
| Instagram Graph API v19+ | OAuth, media publishing, analytics sync | |
| Infrastructure | Docker + Docker Compose | Containerized deployment |
Security
| Category | Implementation | Standard |
|---|---|---|
| Password Hashing | bcrypt, cost factor 12 | OWASP |
| JWT Tokens | HS256, 15-min access TTL, 30-day refresh TTL | RFC 7519 |
| Instagram Tokens | AES-256-CBC encrypted at rest | FIPS 140-2 |
| HTTPS | TLS 1.2+ mandatory, HSTS enabled | OWASP A02 |
| CSRF Protection | Laravel SanctumCSRF cookie + Referer check for web routes | OWASP A01 |
| SQL Injection | Eloquent ORM prepared statements — no raw queries | OWASP A03 |
| XSS Prevention | React JSX auto-escaping; API responses Content-Type: application/json | OWASP A03 |
| Rate Limiting | 100 req/min per user; 5 login attempts per 15 min per IP | OWASP A04 |
| File Upload Security | MIME type whitelist, file extension check, ClamAV scan | OWASP A08 |
| RBAC | Policy-based authorization — user can only access their own resources | Least Privilege |
| Audit Logging | All create/update/delete operations logged in activity_logs | SOC 2 |
| Environment Secrets | .env file, never committed to VCS. Production uses secret manager. | 12-Factor App |
OAuth Security Flow
Performance
| Metric | Target | Strategy |
|---|---|---|
| API Response Time | < 300ms (p95) | DB indexes, eager loading, response caching |
| Page Load (LCP) | < 2 seconds | React code splitting, lazy loading, CDN for static assets |
| Calendar Render | < 1 second | Virtualized list, date-scoped queries, client-side cache |
| Media Upload | < 5 seconds for 8MB | Async upload, progress streaming, background thumbnail generation |
| Queue Processing | < 60 seconds | Parallel workers (Horizon), priority queues, job timeout settings |
| AI Response | < 5 seconds | OpenAI streaming, client-side loading state, timeout 30s |
| Database Queries | < 50ms | Proper indexes, avoid N+1 (eager load), query result caching |
Caching Strategy
| Data | Cache Layer | TTL |
|---|---|---|
| User session data | Redis | 15 minutes |
| Dashboard stats | Redis | 5 minutes |
| Calendar events (day) | Client localStorage | 1 minute |
| Analytics data | Redis | 1 hour |
| Instagram account list | Redis | 5 minutes |
Database Optimization
- Composite index on
scheduled_posts(status, publish_at_utc)for queue worker query - Composite index on
scheduled_posts(user_id, status)for user dashboard - Eager load instagram_account when listing scheduled_posts
- Paginate all list endpoints — max 50 records per page
- Soft-deleted records excluded from all queries via global scope
- Analytics table partitioned by month for large-scale data
Testing
| Test Level | Tool / Framework | Coverage Target | Responsibility |
|---|---|---|---|
| Unit Tests | PHPUnit (Backend), Jest (Frontend) | > 80% (Core Logic) | Developers |
| Integration Tests | PHPUnit | API Endpoints, Queue Workers | Developers |
| UI/E2E Tests | Cypress | Critical Paths (Login, Schedule Post) | QA Automation |
| Security Tests | OWASP ZAP | Vulnerability Scanning | Security Team |
| Performance Tests | k6 | API Load (1k req/sec) | DevOps |
Critical Test Scenarios
- Schedule a post across daylight saving time boundary
- Simulate Instagram API failure and verify exponential backoff retry
- Test duplicate idempotency key submission (should reject second request)
- Upload 100MB MP4 file and verify processing time
- Verify session termination across all devices on password reset
QA Checklist
Frontend (React)
- Responsive design (Mobile/Tablet/Desktop)
- Form validation (Empty fields, invalid data)
- Timezone rendering matches OS local vs selected
- Drag & Drop works smoothly on touch devices
Backend (API)
- Rate limiter blocks > 100 req/min
- Invalid JWT returns 401 Unauthorized
- Past date scheduling returns 422
- Pagination works (page=2, limit=50)
Worker (Publishing)
- Job picks up at exact scheduled_at UTC time
- Failed job increments retry_count
- Max retry limit marks job as failed
- Idempotency prevents double publish
Developer Checklist
- Ensure all new API endpoints are documented in Swagger/OpenAPI
- Verify no N+1 query issues using Laravel Telescope
- Write database migrations with both
up()anddown()methods - Add appropriate indexes for any new foreign keys
- Use strict types (
declare(strict_types=1);) in all new PHP files - Ensure all frontend components use TypeScript interfaces for props
- Remove all
console.loganddd()before creating PR - Run PHP CodeSniffer and ESLint locally
DevOps Checklist
- Configure Redis maxmemory policy to
noevictionfor Queue data safety - Set up Supervisor to manage Laravel Horizon worker processes
- Configure Nginx client_max_body_size to 120M for video uploads
- Set up Datadog/NewRelic APM for API response monitoring
- Ensure S3 / Local storage directories have correct RWX permissions
- Configure automated daily backups for MySQL database
- Set up Slack alerts for Queue worker crashes
- Configure SSL/TLS certificates and force HTTPS redirection
Risks
| Risk | Impact | Probability | Mitigation Strategy |
|---|---|---|---|
| Instagram API changes rate limits | High | Medium | Implement dynamic rate limit detection and job backoff. |
| Queue Worker Memory Leaks | Medium | Medium | Configure supervisor to restart workers every 1000 jobs. |
| OpenAI API Downtime | Medium | Low | Graceful UI degradation; allow manual caption entry. |
| User Timezone Confusion | High | Medium | Clear UI indicators of which timezone is currently selected. |
Assumptions
- Users have valid Instagram Professional/Creator accounts linked to Facebook Pages.
- Average video size will be under 50MB (max allowed is 100MB).
- Most users will schedule posts 1-7 days in advance.
- Users understand that AI content may require manual review before publishing.