DevStack Directory Docs
Complete documentation for installing, configuring, and running your DevStack Directory. No Composer, no Node, no build step — upload and run.
Overview
DevStack Directory is a self-hosted developer tool directory CMS. It supports two listing tiers — free (backlink required, auto-approved on verification) and paid (Polar.sh payment, priority manual review). Everything runs on PHP + SQLite with no external dependencies.
Out of the box you get: user registration with email verification, Google + GitHub OAuth, SMTP email, Polar.sh payments, reCAPTCHA, backlink verification, badge embed codes, OG image + favicon upload, and a full admin panel.
Requirements
- PHP 8.1 or higher — uses
match(),str_contains(), named arguments - PHP extensions:
sqlite3,pdo_sqlite,gd,curl - Apache with
mod_rewriteenabled, or Nginx - Write permission on the
data/directory
Any shared cPanel/Plesk host, VPS, or dedicated server. InfinityFree, Hostinger, SiteGround, DigitalOcean, Hetzner — all work. Minimum PHP 8.1.
Installation
cms_fixed/ folder to your web host's public folder (e.g. public_html/) via FTP or cPanel File Manager. Do not upload the data/ folder — it is created automatically.data/ and data/uploads/ to 755 permission. On cPanel, right-click in File Manager → Change Permissions → 755.data/directory.db with sample categories and default settings.https://yourdomain.com/admin. Default password: admin. You will be prompted to change it on first login.First setup
After logging in, go to Admin → Settings and configure at minimum:
- Site name and tagline
- Badge domain — your full domain without
https://(e.g.yourdomain.com) - From address — sender email for outgoing mail
- SMTP credentials — required for email to work (see Email / SMTP)
- Polar.sh credentials — required for paid listings (see Polar.sh)
The default password is admin. Change it immediately in Admin → Settings → Admin Password before making the site public.
Admin panel
The admin panel is at /admin. It has five sections accessible from the left sidebar.
Tools
Full list of submitted tools with filters by status and tier. Actions per tool:
- Approve — makes the listing live on the directory
- Reject — removes from public view; user can edit and resubmit
- Delete — permanently removes the listing
- Recheck backlink — re-runs backlink verification for free listings
Paid listings are marked with a PAID badge so you can prioritise them in your review queue.
Categories
Create, edit, and delete categories. Each category has a name, slug, and optional icon (emoji or short text). Categories appear as filter pills on the homepage. Deleting a category does not delete its tools — they become uncategorised.
Users
List of all registered users with email, registration date, verification status, and listing count. You can suspend accounts from this panel — suspended users cannot log in.
Settings reference
| Setting | Description |
|---|---|
| site_name | Displayed in navbar, page titles, and emails |
| site_tagline | Shown on the homepage hero section |
| site_description | Meta description tag for SEO |
| site_meta_keywords | Meta keywords tag for SEO |
| badge_domain | Your domain without https:// — used for badge URLs and webhook display |
| paid_price | Display price shown on submit page. Must match your Polar product price. |
| site_favicon | Uploaded favicon file (png, ico, svg, webp) |
| site_og_image | Uploaded 1200×630 OG image for social sharing |
| mail_from | Sender address for all outgoing email |
| smtp_host | SMTP server hostname |
| smtp_port | SMTP port — 587 for TLS, 465 for SSL |
| smtp_user | SMTP username (usually your email) |
| smtp_pass | SMTP password |
| smtp_encryption | tls (recommended), ssl, or none |
| polar_sandbox | Enable when testing with Polar sandbox. Disable in production. |
| polar_project_id | Checkout Link ID from Polar (starts with polar_cl_) |
| polar_webhook_secret | Webhook secret from Polar → Settings → Webhooks |
| recaptcha_enabled | Toggle to enable/disable reCAPTCHA on forms |
| recaptcha_site_key | From Google reCAPTCHA admin console |
| recaptcha_secret_key | From Google reCAPTCHA admin console |
| google_oauth_enabled | Toggle to enable Google OAuth login button |
| google_client_id | From Google Cloud Console → Credentials |
| google_client_secret | From Google Cloud Console → Credentials |
| github_oauth_enabled | Toggle to enable GitHub OAuth login button |
| github_client_id | From GitHub → Settings → Developer settings → OAuth Apps |
| github_client_secret | From GitHub → Settings → Developer settings → OAuth Apps |
Email / SMTP
All transactional emails (verification, password reset) go through SMTP. PHP's mail() is used as a fallback when SMTP is not configured, but most modern hosts require SMTP for reliable delivery.
Recommended: SendPulse
Free tier: 15,000 emails/month. No daily cap. No domain ownership verification required to start.
noreply@yourdomain.com addresssmtp.sendpulse.com · Port: 587 · Encryption: TLS · Username & Password from step 2 · From address: your verified senderTesting: Mailtrap
For staging and testing, Mailtrap catches all outgoing emails in a test inbox — nothing reaches real users. Sign up, go to Email Testing → My Inbox → SMTP Settings, copy the credentials, and paste into admin settings. When going live, swap for SendPulse.
Email triggers
| Event | Recipient | Description |
|---|---|---|
| Registration | New user | Verification link, expires in 24 hours |
| Login (unverified) | User | Fresh verification link automatically resent |
| Resend verification | User | Manual resend from profile dashboard (rate limited: once per 5 min) |
| Password reset | User | Reset link, expires in 1 hour |
Polar.sh payments
Polar.sh handles checkout for Pro listings. The integration supports both sandbox (testing) and production modes via a toggle in admin settings.
Setup steps
https://yourdomain.com/profile/index.php?paid=1. Copy the checkout link ID (the part after /checkout/ — starts with polar_cl_).https://yourdomain.com/webhook/polar.php. Format: Raw. Events: check order.created and order.paid. Copy the webhook secret.Payment flow
- User selects Pro listing and submits the tool form
- They are redirected to Polar checkout
- After successful payment, Polar redirects them back to their profile with a success message
- Listing shows as PAID PENDING REVIEW in their dashboard
- Admin reviews and approves — paid listings are priority
Uncheck Sandbox mode in Admin → Settings and update the webhook URL in Polar to your production domain when you're ready to accept real payments.
Social login
Google OAuth
https://yourdomain.comAuthorized redirect URIs:
https://yourdomain.com/profile/oauth_google.phpThe redirect URI in Google Console must match exactly — including .php extension. https://yourdomain.com/profile/oauth_google.php is correct. Propagation after adding/changing URIs can take a few minutes.
GitHub OAuth
https://yourdomain.comAuthorization callback URL:
https://yourdomain.com/profile/oauth_github.phpOAuth behavior
- If the email from Google/GitHub matches an existing account, the user is logged in to that account
- If no account exists, one is created automatically with email already verified
- OAuth users don't have a password — they can request a password reset if they want email/password access
reCAPTCHA
Google reCAPTCHA v2 ("I'm not a robot" checkbox) on the submission and registration forms.
reCAPTCHA displays on any domain but only validates on registered domains. For staging testing, either register the staging domain or add localhost to the allowed list in Google's console.
Backlink verification
Free listings require a backlink from the submitter's site. The system verifies this automatically.
- User submits a free listing and provides the URL of the page where their badge/link is placed
- The system fetches that page using PHP's
curland searches for a link containing your domain - If found → listing is AUTO-APPROVED and goes live immediately
- If not found → listing goes to PENDING for manual review
From Admin → Tools, click Recheck on any free listing to re-run verification. There is also a bulk recheck option in Admin → Settings that processes all pending free listings at once.
Badge system
The badge lets submitters display a "Listed on [YourDirectory]" badge on their site, satisfying the backlink requirement.
After submission, users get two embed options — dark and light variants. The embed code is a fully inline-styled <a> tag that works on any external site without needing CSS from your directory.
Badge endpoint
/badge.php generates an SVG badge image. Parameters: style=dark (default) or style=light
Set the Badge domain in Admin → Settings before launching. Badge URLs and embed codes use this value. If it's empty, the webhook URL and badge previews will break.
Listing flow
Free listing
Submit form
→ System fetches backlink URL
→ Link to your domain found?
✓ Yes → Auto-approved → Live immediately
✗ No → Pending → Admin review → Approve / Reject
Pro listing
Submit form
→ Redirect to Polar checkout
→ Payment complete
→ Return to profile (/profile/index.php?paid=1)
→ Success message shown
→ Listing: PAID / PENDING REVIEW
→ Admin sees PAID badge → Priority review → Approve / Reject
Status meanings
| Status | Meaning |
|---|---|
| pending | Awaiting admin review or backlink verification |
| approved | Live and visible on the directory |
| rejected | Not approved — user can edit and resubmit |
User accounts
Registration methods
- Email + password — requires email verification before login
- Google OAuth — auto-verified, no password set
- GitHub OAuth — auto-verified, no password set
Email verification flow
- Link sent on registration, expires in 24 hours
- Login attempt with unverified account → fresh link resent automatically
- Resend button available in profile dashboard (rate limited: once per 5 minutes)
- Clicking an expired link shows instructions to sign in and request a new one
Password reset
Available at /profile/forgot. Sends a reset link valid for 1 hour. Enter email, receive link, set new password.
File structure
Moving to a new domain
The database carries all settings, listings, users, and categories — nothing needs reconfiguring except domain-specific fields.
- Upload all files to the new host
- Upload
data/directory.dbanddata/uploads/separately - In Admin → Settings update: Badge domain, From address
- In Polar dashboard → Settings → Webhooks: update the webhook URL to the new domain
- In Google Cloud Console: add new domain to Authorized JavaScript origins and new callback URI to Authorized redirect URIs
- In GitHub OAuth App: update Homepage URL and Authorization callback URL
- In Google reCAPTCHA admin: add the new domain to allowed domains
data/directory.db is your entire database — one file. Back it up regularly. All settings, users, listings, and categories are in this file.
Nginx configuration
If using Nginx instead of Apache, add the following server block:
server {
listen 80;
server_name yourdomain.com;
root /var/www/html;
index index.php;
# PHP handling
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# Block direct access to database
location /data/ { deny all; }
# Allow webhook endpoint
location /webhook/ {
try_files $uri $uri/ =404;
}
location / {
try_files $uri $uri/ =404;
}
}
Security notes
- data/ directory has a
.htaccessblocking PHP execution and directory listing — keep this in place - Admin password stored as a bcrypt hash in the database
- User passwords stored as bcrypt hashes
- CSRF tokens verified on all POST forms
- SQL queries use PDO prepared statements throughout — no raw string interpolation
- Webhook signature verified via HMAC-SHA256 before processing any Polar events
- OAuth state parameter verified to prevent CSRF on OAuth flows
- Uploaded files stored in
data/uploads/which blocks PHP execution
Recommended before going live
- Change the default admin password
- Enable reCAPTCHA to reduce spam submissions
- Configure SMTP —
mail()fallback is unreliable on most hosts - Back up
data/directory.dbregularly — it contains everything - Enable HTTPS — most hosts offer free SSL via Let's Encrypt through cPanel