What's in the box
ZinklySocial is a professional Laravel 13 social / forum application with real-time features, a full admin panel, and a built-in ZIP-based update system. It's the core platform that ZinklyChat, EchoAI, NotiLive and LingoSync all plug into over its REST + WebSocket API.
What's included
Core features
- Timeline, posts, comments, reactions
- Follow system with privacy controls
- Public profiles with walls
- Real-time notifications
- Stories — 24-hour ephemeral content
- Reels — short videos
- Direct messages & group chats
- LiveKit audio / video calls
- Marketplace with VIP packages
- PayPal payments
Admin features
- User management — ban, promote, restrict
- Reports & moderation
- Announcements
- Static pages — Terms, Privacy
- Settings & SEO
- Integrations — Firebase, Google, and more
- Update system with ZIP packages
- Backup & rollback
- Audit logs
- VIP package management
Technology stack
Backend
Laravel 13 (PHP 8.2+), MySQL/MariaDB, Redis for cache & queues, Laravel Sanctum for API auth.
Frontend
Blade templates, Tailwind CSS, Alpine.js for interactivity, Vite for asset bundling.
Services
Firebase Cloud Messaging, LiveKit (WebRTC calls), GIPHY, PayPal, Google OAuth.
Requirements
Every failed install traces back to one of these three things — check them before you upload anything.
Minimum requirements
PHP 8.2+
Composer 2.x
Node.js 18+
with npm
MySQL 8.0+
or MariaDB 10.6+
Redis 6+
queues, cache, sessions
Nginx or Apache
web server
SSL certificate
Let's Encrypt recommended
A Firebase project
free tier — for push notifications, see below
Required PHP extensions
Recommended server specs
| Tier | vCPU | RAM | Storage | Redis | Notes |
|---|---|---|---|---|---|
| Small — up to 1K users | 2 | 4 GB | 50 GB SSD | 1 GB | — |
| Medium — up to 10K users | 4 | 8 GB | 100 GB SSD | 2 GB | Separate DB server recommended |
Installation
Pick the guide that matches your access level below, then follow it start to finish — don't mix steps between guides.
What's in the download: the main app ZIP (e.g. zinkly-social-v1.0.0.zip) is the whole application — extract it on the server, never straight into a folder your web server already serves. A separate small installer-only ZIP contains just installation.php for the FTP-only path; if you have SSH, ignore it.
I have SSH access
Generic Linux/VPS steps — Composer, artisan, npm. Works on any host that gives you a shell.
I'm on HestiaCP
The exact layout this app runs on in production — app in public_html, document root pointed at public_html/public.
HestiaCP InstallationNo SSH at all
Shared hosting with only FTP / File Manager — still ends with the document root pointed at public/.
No SSH (FTP only)No SSH, no artisan at all
Shared hosting with only phpMyAdmin/FTP — import the schema as raw SQL instead of running migrations.
Covered separately under AdminCP → Documentation → Manual SQL Setup once you can reach it.
SSH Installation
The recommended way to install ZinklySocial — a full install on a fresh Linux server (Ubuntu/Debian) with SSH access.
- Connect and upload
# from your machine ssh youruser@your-server-ip # create the app directory mkdir -p /home/user/web/yourdomain.com/zinkly-app cd /home/user/web/yourdomain.com # upload zinkly-social.zip here (scp / sftp), then: unzip zinkly-social.zip -d zinkly-app cd zinkly-app - Install dependencies
# PHP dependencies — no dev packages in production composer install --no-dev --optimize-autoloader # JS dependencies + build frontend assets npm install npm run build - Configure environment
cp .env.example .env php artisan key:generate nano .env # set DB_*, APP_URL, MAIL_*, etc. - Database & storage
php artisan migrate --force --seed php artisan storage:link - Permissions & caching
chmod -R 775 storage bootstrap/cache chown -R www-data:www-data storage bootstrap/cache # match your web server's user php artisan config:cache php artisan route:cache php artisan view:cache - Point the web server — the document root must point at the app's public/ folder, never the app root.
# example Nginx root root /home/user/web/yourdomain.com/zinkly-app/public;
HestiaCP Installation
The app lives directly inside public_html; HestiaCP's Nginx vhost is pointed at public_html/public as the document root. One folder, no file-shuffling.
- Create the domain and database
- HestiaCP → Web → Add Web Domain (e.g.
social.yourdomain.com) - Once DNS resolves to the server, enable SSL → Let's Encrypt
- HestiaCP → DB → Add Database — note the generated name, username, password
- HestiaCP → Web → Add Web Domain (e.g.
- Upload the app straight into public_html
ssh user@your-server-ip cd /home/user/web/social.yourdomain.com/public_html # upload zinkly-social.zip here, then: unzip zinkly-social.zip -d . rm zinkly-social.zip composer install --no-dev --optimize-autoloader npm install && npm run build - Point the document root at public_html/public — HestiaCP serves public_html by default; Laravel must never be web-accessible at its root.
Newer HestiaCP (UI method — no SSH needed for this step): HestiaCP → Web → your domain → Edit → check Custom document root → Point to: your domain → Directory:
public. HestiaCP shows the resulting path live and saves/reloads Nginx for you:/home/user/web/social.yourdomain.com/public_html/publicOlder HestiaCP without that toggle — edit the vhost by hand instead:
nano /home/user/conf/web/social.yourdomain.com/nginx.ssl.conf # find the "root" line and point it at: root /home/user/web/social.yourdomain.com/public_html/public; nginx -t && systemctl reload nginx - Configure & migrate
cd /home/user/web/social.yourdomain.com/public_html cp .env.example .env php artisan key:generate nano .env # DB_DATABASE, DB_USERNAME, DB_PASSWORD → from HestiaCP → DB # APP_URL=https://social.yourdomain.com php artisan migrate --force --seed php artisan storage:link chmod -R 775 storage bootstrap/cache chown -R user:user storage bootstrap/cache # the HestiaCP system user php artisan config:cache && php artisan route:cache && php artisan view:cache
The "Custom document root" toggle has its own PHP field — don't leave it blank. HestiaCP applies the docroot to both Nginx and PHP-FPM's own working directory/open_basedir. If that PHP-side path is left empty or also gets set to just public/, PHP's working root shrinks to public/ only — Laravel then can't reach storage/, bootstrap/cache/, or vendor/ outside it, and breaks entirely (500 errors on everything). Make sure that field points at the app root (public_html), not public_html/public.
Common pitfall: editing the domain again in the HestiaCP UI regenerates the Nginx config and can silently reset the document root back to plain public_html. Re-check it after any change made there. PHP-FPM also runs as your HestiaCP login user, not www-data — chown accordingly.
No SSH at all (FTP / File Manager only)
The document root still has to end up pointing at the app's public/ folder — that never goes away, SSH or not. Editing index.php's paths to fake it is fragile and breaks on the next update, so don't do that.
- Upload and extract the main ZIP outside your public web folder if your panel allows it (a sibling folder to public_html) — matches the SSH/HestiaCP layout, done over FTP.
- Point the document root at that folder's public/ subfolder, using your host's panel (usually under "Advanced" or "Domain Settings" — no SSH needed). No such field? Ask your host's support — it's a one-line change on their end.
- Only if truly impossible: upload the main ZIP's contents directly into public_html, then from the small installer ZIP upload just
installation.phpinto that same folder. - Add the protection block below to the top of public_html/.htaccess regardless of which option you used.
- Visit
https://yourdomain.com/installation.phpto configure the database. - Then visit
https://yourdomain.com/installto finish setup, run migrations, and create your admin account.
.htaccess protection (add regardless of the option above)
# top of public_html/.htaccess, above Laravel's own rules
RewriteRule ^(app|bootstrap|config|database|lang|resources|routes|storage|tests|vendor|\.env|composer\.(json|lock))(/.*)?$ - [F,L]
Option 3 still leaves the app root web-accessible at the domain root even with this rule — it only blocks a specific path list, not everything. Options 1–2 (a real document-root change) are the only way to fully avoid exposing application code. Use SSH whenever you possibly can.
Firebase & Push Notifications
ZinklySocial sends browser push notifications for new messages, incoming calls, likes, comments, and follows — even when the site isn't open in a tab. This needs its own Firebase project; it's a separate setup step from the server install above, and the app runs fine without it, just without push.
1. Create a Firebase project
- Create the project at console.firebase.google.com → Add project.
- Register a web app — Add app → Web, name it after your site, and copy the SDK config it shows you: API key, Project ID, Auth domain, Storage bucket, Messaging sender ID, App ID.
- Generate a Web Push certificate — Project settings (gear icon) → Cloud Messaging tab → under "Web configuration" → generate one. This is your VAPID key.
- Generate a service account key — Project settings → Service accounts tab → Generate new private key. This downloads a JSON file — keep it secret, it's used server-side only.
Building a separate native mobile app (e.g. ZinklyChat)? You'd also add an Android app (for google-services.json) and/or an iOS app (for GoogleService-Info.plist) to this same Firebase project — but that only matters for that separate mobile codebase, not for this web install, and can be skipped entirely if you're not shipping a native app.
2. Configure the credentials
Go to AdminCP → Integrations and fill in the Firebase section with the web app values from step 1:
| AdminCP field | Firebase console value |
|---|---|
Project ID | Project ID |
Web API key | apiKey |
Auth domain | authDomain |
Storage bucket | storageBucket |
Messaging sender ID | messagingSenderId |
App ID | appId |
VAPID key | Web Push certificate key pair (step 1.3) |
These save to the database and take effect immediately — no deploy or cache clear needed — and override any matching FIREBASE_WEB_* values in .env if both are set.
The service account JSON from step 1.4 is different: a server-side secret, and it is never entered into AdminCP. Upload it somewhere outside your public web root, then set one .env value:
FIREBASE_CREDENTIALS=/absolute/path/outside/public_html/firebase-service-account.json
Then pick up the new path:
php artisan config:clear && php artisan config:cache
3. How it turns on for a visitor
Nothing further to configure — this happens automatically once step 2 is saved.
- The browser does not prompt for permission the instant someone logs in — a dismissible "Get notified here" banner appears first; the actual permission prompt only fires after the visitor clicks it.
- Once granted, the browser registers a service worker at
/firebase-messaging-sw.js(already included in the app — nothing to configure), which receives pushes while the tab is closed or backgrounded. - The resulting device token is saved to the
device_tokenstable, which is what sends pushes on the server side.
Testing & troubleshooting
Push notifications require HTTPS — they will not work over plain HTTP, even on staging. A visitor who denied the permission prompt must reset it in their browser's own site settings to be asked again; clicking the banner again won't re-trigger it.
- Send a test push from Firebase Console → Cloud Messaging → Send test message using a registration token from the
device_tokenstable, before debugging app code. - Nothing arriving? Check
storage/logs/laravel.logfor "FCM send failed" — usually a missing/invalidFIREBASE_CREDENTIALSpath or a revoked service account key. - A token FCM reports as invalid (app uninstalled, unsubscribed, site data cleared) is pruned automatically from
device_tokenson the next send — no manual cleanup needed.
Troubleshooting
Fixes for the issues that come up most, in the order they're worth checking.
| Symptom | Likely cause |
|---|---|
| Pages look unstyled — raw text, no colors | Browser cache, a failed/missing npm run build, or a stale view:cache — see below |
| 500 server error / blank white page | Check storage/logs/laravel.log, then the web server's own error log |
| "Permission denied" writing to storage/ | Re-run the chmod/chown step from the install guide |
| "I changed something but nothing happened" | Stale config:cache/view:cache, browser cache, or a queue worker running old code |
| Uploads / avatars 404 instead of loading | The public/storage symlink is missing — run php artisan storage:link |
| Notifications / emails never arrive | Queue worker not running, or SMTP not configured/tested |
| Calls won't connect | LiveKit credentials or server URL wrong under AdminCP → Integrations |
Pages look unstyled — step by step
This means the browser couldn't load app.css/app.js from public/build/. In order of likelihood:
- Stale browser cache — every
npm run buildgenerates new hashed filenames and deletes the old ones; an old cached page pointing at a deleted file 404s. Hard refresh:Ctrl+Shift+R(Cmd+Shift+Ron Mac). - Build never ran or failed — check
public/build/manifest.jsonexists and is recent (ls -la public/build); re-runnpm run buildif missing or stale. - Cached views referencing an old manifest — run
php artisan view:clear(thenview:cachein production) after any frontend rebuild. - Nginx not serving /build/ correctly — confirm the document root actually points at public/, not the app root.
Still stuck? Take a fresh backup before trying anything destructive, then check storage/logs/laravel.log for the exact exception — the message and stack trace almost always point straight at the cause.
More in AdminCP
That's the surface. Once you're logged into AdminCP, System → Documentation carries this same guide plus everything else — environment variables, updates, backups, integrations — and the full version-by-version changelog.