Backend for The Earth App, powered by Drupal 11
This is the second version of the backend system for The Earth App, a comprehensive RESTful API built on top of PHP 8.4 and Drupal 11.3. The module provides a complete backend infrastructure for a social networking platform focused on novelty, activities, and user engagement.
- Overview
- Technical Stack
- Architecture
- Installation
- Core Components
- Security & Performance
- Development
- Testing
Mantle2 is a custom Drupal 11 module that implements a RESTful API backend for The Earth App. It leverages Drupal's entity system, field API, and routing infrastructure while adding custom controllers, services, and event subscribers to create a modern, scalable API platform.
- RESTful API with OpenAPI/Swagger documentation
- User Management with token auth, OAuth, profiles, and social features
- Activity Tracking for environmental activities with custom fields
- Event Management with participation, cancellation, and image submissions
- Prompt System with visibility-aware discovery and response threads
- Article Content with quizzes, moderation, and user attribution
- Gamification with badges, points, quests, and profile cosmetics
- Rate Limiting with configurable per-endpoint and global limits
- CORS Support with origin whitelisting
- Redis Caching with automatic fallback to Drupal cache
- Email Notifications with HTML rendering and verification codes
- PHP: 8.4+
- Drupal Core: 11.3+
- Symfony: 7.3+ (Event Dispatcher, Rate Limiter, Cache)
- Redis: Optional caching layer via
drupal/redismodule - PostgreSQL/MySQL: Database backend (Drupal standard)
- Composer: PHP dependency management
- Bun: JavaScript runtime for development tooling
- PHPUnit: 11.5+ for unit testing
- Drush: 13.7+ for Drupal CLI operations
- PHPStan: Static analysis
- PHP CodeSniffer: Code quality enforcement
- Prettier: Code formatting for PHP, XML, YAML, JSON
{
"drupal/core": "^11.3",
"drupal/json_field": "^1.4", // JSON field storage
"drupal/key": "1.22.0", // API key management
"drupal/smtp": "^1.4", // Email delivery
"drupal/redis": "^1.10", // Redis integration
"drupal/openid_connect": "^3.0@alpha", // OAuth/OpenID providers
"symfony/rate-limiter": "^7.3", // Rate limiting
"symfony/event-dispatcher": "^7.3", // Event system
"symfony/cache": "^7.3" // Cache abstractions
}mantle2/
├── src/
│ ├── Controller/ # API endpoint controllers
│ │ └── Schema/ # OpenAPI schema generators
│ ├── Custom/ # Domain models and enums
│ ├── EventSubscriber/ # Symfony event subscribers
│ ├── Plugin/ # OpenID Connect client plugins
│ └── Service/ # Business logic helpers
├── tests/
│ └── src/
│ ├── Unit/ # PHPUnit tests
│ └── Mocks.php # Test fixtures
├── mantle2.info.yml # Module metadata
├── mantle2.module # Hook implementations
├── mantle2.install # Installation & schema
├── mantle2.routing.yml # API route definitions (200+ endpoints)
├── mantle2.caching.yml # Custom server-side caching definitions
├── mantle2.services.yml # Service container definitions
├── composer.json # PHP dependencies
├── package.json # Dev tooling
└── phpunit.xml.dist # Test configuration
All API endpoints are implemented as controller methods extending Drupal's ControllerBase:
class UsersController extends ControllerBase
{
public function users(Request $request): JsonResponse
{
// Handle paginated user listing
}
public function login(Request $request): JsonResponse
{
// Authenticate and return bearer token
}
}Business logic is encapsulated in helper services registered in mantle2.services.yml:
- GeneralHelper: Common utilities (pagination, validation)
- UsersHelper: User operations and authentication
- ActivityHelper: Activity-related business logic
- PointsHelper: Points, badges, cosmetics, and quest lifecycle
- OAuthHelper: OAuth token validation and provider linking
- CampaignHelper: Email campaign content and placeholder expansion
- CloudHelper: Cloud service requests and websocket notifications
- RedisHelper: Cache abstraction with fallback
Custom PHP classes in src/Custom/ represent business entities:
- Implement
JsonSerializablefor API responses - Enforce validation in constructors
- Provide type-safe interfaces
class Activity implements JsonSerializable
{
protected string $id;
protected string $name;
protected array $types = [];
protected ?string $description = null;
public const int MAX_TYPES = 5;
public function __construct(
string $id,
string $name,
array $types = [],
?string $description = null,
array $aliases = [],
array $fields = [],
) {
if (count($types) > self::MAX_TYPES) {
throw new InvalidArgumentException('Too many activity types');
}
// ... validation and initialization
}
}Symfony's event dispatcher handles cross-cutting concerns:
- RateLimitSubscriber: Pre-request rate limit enforcement
- RateLimitResponseSubscriber: Appends global and endpoint rate headers
- CorsSubscriber: CORS header injection
- ApiExceptionSubscriber: Global error handling
- ResponseCacheSubscriber: Config-driven read-through/invalidation caching
- PostResponseSubscriber: Post-response badge progress tracking
- PHP 8.4 or higher
- Composer 2.x
- Drupal 11.3+ installed and configured
- Redis server (optional, recommended for production)
- SMTP server credentials for email functionality
-
Clone the repository into your Drupal modules directory:
cd /path/to/drupal/modules/custom git clone <repository-url> mantle2 cd mantle2
-
Install PHP dependencies:
composer install
-
Enable required Drupal modules:
drush en node user comment json_field key field options datetime smtp redis \ openid_connect -y
-
Enable mantle2:
drush en mantle2 -y
This runs the installation hooks in
mantle2.installwhich:- Creates custom content types (Activity, Event, Article, Prompt)
- Creates custom comment types (Activity Comments, Article Comments)
- Defines extensive custom fields with JSON storage
- Sets up user profile fields
- Configures field display settings
-
Configure Redis (optional): Edit
settings.php:$settings['redis.connection']['interface'] = 'PhpRedis'; $settings['redis.connection']['host'] = '127.0.0.1'; $settings['redis.connection']['port'] = 6379; $settings['cache']['default'] = 'cache.backend.redis';
-
Configure SMTP (required for email features):
- Navigate to
/admin/config/system/smtp - Enter SMTP server credentials
- Test email delivery
- Navigate to
-
Clear cache:
drush cr
Access the API documentation:
- OpenAPI Schema:
https://your-domain.com/openapi - Swagger UI:
https://your-domain.com/swagger-ui
Test a simple endpoint:
curl https://your-domain.com/v2/helloResponsibilities:
- User CRUD operations
- Authentication (token login/logout and provider-based OAuth)
- Profile management (photos, privacy, account tiers)
- Social graph (friends and circle management)
- Notifications, badges, and points
- Quest lifecycle and cosmetics
- Email verification, unsubscribe, and password reset flows
- Activity, prompt, article, and event associations
Database Queries:
- Uses both Entity API (
$storage->getQuery()) and direct SQL (Drupal::database()) - Random sorting implemented via
orderRandom()for discovery features - Supports search across multiple fields (username, first name, last name)
Responsibilities:
- Activity catalog management
- Type filtering and categorization
- Activity-user associations
Key Features:
- Supports up to 5 activity types per activity
- JSON field storage for flexible metadata
- Full-text search across name, description, aliases
- Randomized and deterministic list retrieval modes
Responsibilities:
- Event lifecycle management
- Participation tracking
- Date-based filtering
- Event visibility and attendee list management
- Event image submission moderation
Features:
- Date range queries for event discovery
- RSVP/signup/leave participation management
- Event cancellation and uncancel flows
- Event type enumeration
- Geographic location and activity tagging support
Responsibilities:
- Prompt catalog and random prompt delivery
- User response collection
- Visibility-aware filtering and moderation
Logic:
- Tracks user responses
- Prevents duplicate responses per user per prompt
- Supports response update/delete and expiration checks
Responsibilities:
- Article content management
- Content moderation
- Author attribution
- Expiration checks and quiz retrieval
Features:
- Rich text content support via JSON fields
- Tag/ocean metadata support
- Role-gated article creation and updates
Utilities:
paginatedParameters(Request): Validates and extracts pagination paramsfindOrdinal(array, enum): Maps enum values to database integersvalidateJson(string): JSON validation- Various format converters and validators
User Operations:
getOwnerOfRequest(Request): Extract authenticated user from requestfindBy(string): Flexible user lookup by ID/username/emailissueToken(UserInterface): Create bearer token with bounded session countgetUserByToken(string): Resolve and validate bearer tokens (with sliding expiry)revokeToken(string): Revoke active authentication token- Friend/circle relationship management
Caching Abstraction:
RedisHelper::set(string $key, array $data, int $ttl = 900): bool
RedisHelper::get(string $key): ?array
RedisHelper::delete(mixed $key): bool
RedisHelper::exists(string $key): bool
RedisHelper::ttl(string $key): int
RedisHelper::list(string $pattern): array
RedisHelper::cache(?string $key, callable $callback, int $ttl = 900): arrayFeatures:
- Automatic fallback to Drupal cache backend if Redis unavailable
- JSON serialization of complex data structures
- TTL support for automatic expiration
- Connection pooling via Drupal Redis module
Usage Example:
// Store email verification code
RedisHelper::set(
"email_verify:{$userId}",
[
'code' => $code,
'email' => $email,
'created' => time(),
],
900,
); // 15 minutes TTL
// Retrieve and validate
$data = RedisHelper::get("email_verify:{$userId}");
if ($data && $data['code'] === $userInputCode) {
// Verify successful
RedisHelper::delete("email_verify:{$userId}");
}Email Rendering:
- Converts markdown to HTML for email templates
- Applies consistent styling
- Handles inline CSS for email clients
- Supports template variables
class Activity implements JsonSerializable
{
protected string $id;
protected string $name;
protected array $types; // ActivityType[]
protected ?string $description;
protected array $aliases; // Alternative names
protected array $fields; // Custom metadata
public const int MAX_TYPES = 5;
}class Event implements JsonSerializable
{
private string $id;
private int $hostId;
private string $name;
private string $description;
private EventType $type;
private array $activities; // Activity[]|ActivityType[]
private float $latitude;
private float $longitude;
private int $date; // Unix timestamp in milliseconds
private ?int $endDate;
private Visibility $visibility;
private array $attendees;
private array $fields;
}class Notification implements JsonSerializable
{
public string $id;
public string $userId;
public string $title;
public string $message;
public ?string $link;
public string $type; // info, warning, error, success
public string $source;
public bool $isRead;
public int $timestamp;
}AccountType:
FREE: Regular userPRO: Pro user tierWRITER: Writer privilegesORGANIZER: Organizer privilegesADMINISTRATOR: Administrative access
Visibility:
PUBLIC: Visible to allUNLISTED: Hidden from broad listing, visible by context/ownerPRIVATE: Only user
Privacy:
PRIVATE,CIRCLE,MUTUAL,PUBLICsettings for profile field visibility
ActivityType and EventType:
- Enumerated types for categorization (notifications use string severity values)
Configuration:
// Global limits
Authenticated: 120 requests / 60 seconds
Anonymous: 60 requests / 60 seconds
// Per-endpoint limits (examples)
POST /v2/users/login: 3 requests / 60 seconds
POST /v2/users/create: 5 requests / 5 minutes
POST /v2/events/create: 3 requests / 2 minutesImplementation:
- Uses Drupal's expirable key-value store
- Separate counters for global and per-endpoint limits
- Environment variable overrides for global limits
- IP-based tracking (Cloudflare-aware)
- Returns
429 Too Many Requestswith retry headers
Headers Added:
X-RateLimit-Limit: 3
X-RateLimit-Remaining: 2
X-RateLimit-Reset: 1698765432
X-Global-RateLimit-Limit: 120
X-Global-RateLimit-Remaining: 119
Allowed Origins:
[
'https://api.earth-app.com',
'https://earth-app.com',
'https://app.earth-app.com',
'https://cloud.earth-app.com',
'capacitor://localhost', // iOS
'http://localhost', // Android
'http://localhost:3000', // Development only
'http://127.0.0.1:3000', // Development only
'http://localhost:3001', // Development only
'http://127.0.0.1:3001', // Development only
];Headers Set:
Access-Control-Allow-Origin: <matched-origin>
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, Accept,
X-Requested-With, X-Admin-Key
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 3600
Vary: Origin
Error Handling:
- Catches unhandled exceptions in API routes
- Converts to consistent JSON error responses
- Logs errors with context
- Prevents sensitive information leakage in production
- Bearer token authentication with issuance/revocation and sliding expiry
- OAuth provider sign-in/linking (Google, Microsoft, Discord, GitHub, Facebook)
- Password hashing using Drupal's password API (bcrypt)
- Password strength validation
- Email verification for account creation
- Admin key validation for privileged operations
- Request parameter sanitization
- JSON schema validation
- SQL injection prevention via Entity API and parameterized queries
- XSS protection through Drupal's filtering system
- File upload validation (type, size, permissions)
- IP-based rate limiting
- Separate limits for authenticated vs. anonymous users
- Per-endpoint rate limiting for sensitive operations
- Configurable time windows and thresholds
- Cloudflare IP detection support
- User-level visibility settings (PUBLIC, UNLISTED, PRIVATE)
- Field-level privacy for profile attributes
- Friend/circle-based content filtering
- Profile photo access control
- Origin whitelist enforcement
- Credentials support for trusted domains
- Preflight request handling
// Redis/key-value for token and short-lived verification data
RedisHelper::set("email_verify:{$userId}", ['code' => $code], 900);
// Entity caching via Drupal cache tags
$users = $storage->loadMultiple($uids);
// Config-driven response caching (mantle2.caching.yml)
// ResponseCacheSubscriber sets X-Cache: HIT/MISS- Indexed fields for common queries (username, email, ID)
- Pagination to limit result sets
- Direct SQL for complex queries (random sorting)
- Query object cloning for count queries to avoid duplication
- User entities loaded on-demand
- Related entities fetched only when needed
- JSON fields decoded on access
// Good: Load multiple entities at once
$users = $storage->loadMultiple($uids);
// Good: Paginated with limit
$query->range($offset, $limit);
// Good: Specific field loading
$query->fields('u', ['uid', 'name', 'mail']);Drupal Logger Integration:
Drupal::logger('mantle2')->error('Error message', ['context' => $data]);
Drupal::logger('mantle2')->warning('Warning message');
Drupal::logger('mantle2')->info('Info message');Logged Events:
- Failed login attempts
- Rate limit violations
- Email delivery failures
- Redis connection issues
- API exceptions
- User registrations
- Password changes
-
Install dependencies:
composer install bun install
-
Configure local environment:
// settings.local.php $config['system.logging']['error_level'] = 'verbose'; $settings['redis.connection']['host'] = 'localhost';
-
Enable development modules:
drush en devel devel_generate dblog -y
-
Generate test data:
drush devel-generate-users 50 drush devel-generate-content 100 --types=activity,event,article
Formatting:
# PHP, YAML, XML, JSON
bun run prettier # Format all files
bun run prettier:check # Check formatting
# PHP-specific
vendor/bin/phpcbf # Auto-fix coding standards
vendor/bin/phpcs # Check coding standardsStatic Analysis:
vendor/bin/phpstan analyse src/Pre-commit Hooks:
Configured via Husky and lint-staged in package.json:
{
"lint-staged": {
"*.{php,xml,dist,json,install,module,yml,md}": "prettier --write"
}
}Generate OpenAPI Schema:
Visit /openapi to see auto-generated schema based on route definitions in mantle2.routing.yml.
Interactive Swagger UI:
Visit /swagger-ui for interactive API testing and documentation.
Schema Annotations: Routes include OpenAPI metadata:
mantle2.users:
path: '/v2/users'
options:
tags: Users
description: Retrieves a list of Earth App users
schema/200: '#Users'
schema/400: Invalid Pagination Parameters
query: # Query parameter schema
limit:
type: integer
minimum: 1
maximum: 100Enable Verbose Errors:
// settings.local.php
$config['system.logging']['error_level'] = 'verbose';
error_reporting(E_ALL);
ini_set('display_errors', true);Database Queries:
drush watchdog:show --type=mantle2
drush ws --tail # Live log tailClear Caches:
drush cr # Full cache rebuild
drush cc views # Clear specific bin
drush redis-cli flushall # Clear RedisLocation: phpunit.xml.dist
Run Tests:
# All tests
vendor/bin/phpunit
# Specific test file
vendor/bin/phpunit tests/src/Unit/GeneralUnitTest.php
# With coverage
vendor/bin/phpunit --coverage-html coverage/Test Structure:
namespace Drupal\Tests\mantle2\Unit;
use PHPUnit\Framework\TestCase;
use Drupal\mantle2\Service\GeneralHelper;
class GeneralUnitTest extends TestCase
{
public function testFormatId()
{
$this->assertEquals('000000000000000000000123', GeneralHelper::formatId(123));
}
}Location: tests/src/Mocks.php
Provides test fixtures for:
- User entities
- Activity nodes
- Event nodes
- Request objects
- Service mocks
Use Drush:
# Test API endpoints
drush php-eval "print_r(\Drupal::service('http_kernel')->handle(Request::create('/v2/hello')));"
# Test services
drush php-eval "print(\Drupal\mantle2\Service\GeneralHelper::formatId(123));"Using cURL:
# Health check
curl http://localhost/v2/hello
# Login
curl -X POST http://localhost/v2/users/login \
-H "Content-Type: application/json" \
-d '{"username":"testuser","password":"testpass"}'
# Get users (authenticated)
curl http://localhost/v2/users \
-H "Authorization: Bearer <token>"Using Postman/Insomnia:
Import the OpenAPI schema from /openapi for automatic request generation.
See LICENSE file for details.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Run tests and formatting (
vendor/bin/phpunit && bun run prettier:check) - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
For issues, questions, or contributions, please open an issue on the GitHub repository.
Built with ❤️ for The Earth App