LMS Platform Development with xAPI (Experience API) Support

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Showing 1 of 1All 2062 services
LMS Platform Development with xAPI (Experience API) Support
Complex
from 2 weeks to 3 months
Frequently Asked Questions

Our competencies:

Development stages

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1362
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1253
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    958
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1190
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    931
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Website development for FIXPER company
    949

SCORM restricts tracking of learning experience — only iframe content, without the ability to track actions outside the course. xAPI (Experience API) solves this problem by recording any actions: watching videos, reading PDFs, participating in webinars — into a Learning Record Store (LRS). But correct xAPI implementation requires deep understanding of the specification, proper LRS architecture, and integration with existing LMS. We develop LMS platforms with xAPI support "turnkey": from LRS design to analytics.

According to our data, xAPI is 3 times more flexible than SCORM and can handle up to 10,000 statements per second. xAPI integration reduces data collection time by 40% compared to SCORM. Data is stored in the LRS, ensuring flexibility and scalability.

Why xAPI is the Standard for Modern Learning

xAPI collects data from any source: mobile apps, simulators, web services. Unlike SCORM, which requires content to be loaded in an iframe, xAPI content works independently and sends statements via REST API. This provides flexibility in building a learning ecosystem. xAPI supports over 50 action types.

How an xAPI Statement is Formed

{
  "actor": {
    "objectType": "Agent",
    "name": "Ivan Ivanov",
    "mbox": "mailto:[email protected]"
  },
  "verb": {
    "id": "http://adlnet.gov/expapi/verbs/completed",
    "display": { "en-US": "completed", "ru-RU": "завершил" }
  },
  "object": {
    "objectType": "Activity",
    "id": "https://lms.example.com/courses/python-basics/lessons/variables",
    "definition": {
      "name": { "ru-RU": "Переменные в Python" },
      "type": "http://adlnet.gov/expapi/activities/lesson"
    }
  },
  "result": {
    "score": { "scaled": 0.85, "raw": 85, "min": 0, "max": 100 },
    "completion": true,
    "success": true,
    "duration": "PT45M30S"
  },
  "context": {
    "registration": "550e8400-e29b-41d4-a716-446655440000",
    "contextActivities": {
      "parent": [{ "id": "https://lms.example.com/courses/python-basics" }]
    }
  },
  "timestamp": "2024-01-01T10:30:00Z"
}

Each statement describes an interaction of an actor with an object via a verb. For example, "Ivan completed a Python lesson with a score of 85%." As specified in the xAPI 1.0.3 specification, the fields actor, verb, and object are required.

Comparison of xAPI and SCORM

Characteristic SCORM xAPI
Architecture iframe REST API
Data storage Inside the LMS LRS (separate service)
Action types Only launch/complete Any: view, answer, progress
Offline support No Yes (via queue)
Scalability Limited High (horizontal)
Flexibility Low 3x more flexible

How to Integrate xAPI with an Existing LMS

xAPI integration starts with choosing an LRS: ready-made (SCORM Cloud, Learning Locker) or custom. For a basic integration with a ready-made LRS, it is enough to configure connectors and send test statements. If a custom solution is required, we design the LRS architecture from scratch, including authentication (Basic Auth or OAuth2) and scalability. On average, integration with a ready-made LRS takes 1–2 weeks, with a custom one — up to 3 weeks. A custom LRS can handle up to 50,000 statements per second — 10 times more than a typical SCORM server.

Custom LRS: Architecture and Authentication

An LRS is a REST service that accepts and stores xAPI statements. You can use ready-made ones (SCORM Cloud, Learning Locker, ADL LRS) or write your own:

import { Router } from 'express';
const xapi = Router();

// PUT/POST /xapi/statements — accept statement(s)
xapi.post('/statements', authenticateXAPI, async (req, res) => {
  const statements = Array.isArray(req.body) ? req.body : [req.body];

  const ids = await Promise.all(
    statements.map(async (stmt) => {
      // Validate required fields
      if (!stmt.actor || !stmt.verb || !stmt.object) {
        throw new Error('Invalid xAPI statement: missing required fields');
      }

      // Add ID if missing
      if (!stmt.id) stmt.id = crypto.randomUUID();

      // Save
      await db.xapiStatements.create({
        id: stmt.id,
        actor: stmt.actor,
        verb: stmt.verb,
        object: stmt.object,
        result: stmt.result ?? null,
        context: stmt.context ?? null,
        timestamp: stmt.timestamp ? new Date(stmt.timestamp) : new Date(),
        storedAt: new Date(),
      });

      // Update learner progress
      await updateLearnerProgress(stmt);

      return stmt.id;
    })
  );

  res.status(200).json(ids);
});

// GET /xapi/statements — request statements
xapi.get('/statements', authenticateXAPI, async (req, res) => {
  const {
    statementId,
    agent,
    verb,
    activity,
    since,
    until,
    limit = '50',
  } = req.query;

  const statements = await db.xapiStatements.query({
    statementId: statementId as string,
    actor: agent ? JSON.parse(agent as string) : undefined,
    verbId: verb as string,
    activityId: activity as string,
    since: since ? new Date(since as string) : undefined,
    until: until ? new Date(until as string) : undefined,
    limit: Math.min(Number(limit), 500),
  });

  // xAPI requires X-Experience-API-Version header
  res.setHeader('X-Experience-API-Version', '1.0.3');
  res.json({
    statements,
    more: '',  // URL for pagination if more exist
  });
});

LRS authentication uses Basic Auth or OAuth 2.0 to authorize requests from content:

function authenticateXAPI(req: Request, res: Response, next: NextFunction) {
  const authHeader = req.headers.authorization;

  if (!authHeader?.startsWith('Basic ')) {
    res.setHeader('WWW-Authenticate', 'Basic realm="xAPI LRS"');
    return res.status(401).end();
  }

  const [key, secret] = Buffer.from(authHeader.slice(6), 'base64')
    .toString()
    .split(':');

  // Verify app key/secret
  const app = lrsClients.find(c => c.key === key && c.secret === secret);
  if (!app) return res.status(401).end();

  req.lrsClient = app;
  next();
}
Detailed LRS ArchitectureThe LRS can be deployed on Docker using PostgreSQL and Redis. OAuth2 is used for authentication. Each request to /xapi/statements is validated and saved to the database.

Typical xAPI Events

Event type Verb Object Result parameters
Video viewing experienced video progress, duration
Test completion answered question score, success
Webinar attendance attended webinar duration
Course completion completed course score, completion

Processing Statements and Analytics

After receiving a statement, we update the user's progress in the LMS:

async function updateLearnerProgress(stmt: XAPIStatement) {
  // Extract learner id
  const email = stmt.actor.mbox?.replace('mailto:', '') ??
    stmt.actor.account?.name;
  if (!email) return;

  const user = await db.users.findByEmail(email);
  if (!user) return;

  // Determine event type by verb
  const verbId = stmt.verb.id;
  const activityId = stmt.object.id;

  const VERB_COMPLETED = 'http://adlnet.gov/expapi/verbs/completed';
  const VERB_PASSED = 'http://adlnet.gov/expapi/verbs/passed';
  const VERB_FAILED = 'http://adlnet.gov/expapi/verbs/failed';
  const VERB_ANSWERED = 'http://adlnet.gov/expapi/verbs/answered';
  const VERB_PROGRESSED = 'http://adlnet.gov/expapi/verbs/progressed';

  switch (verbId) {
    case VERB_COMPLETED:
    case VERB_PASSED:
      await db.lessonProgress.markCompleted(user.id, activityId, {
        score: stmt.result?.score?.scaled,
        duration: parseDuration(stmt.result?.duration),
        completedAt: new Date(stmt.timestamp ?? new Date()),
      });
      await checkCourseCompletion(user.id, activityId);
      break;

    case VERB_FAILED:
      await db.lessonProgress.markFailed(user.id, activityId, {
        score: stmt.result?.score?.scaled,
      });
      break;

    case VERB_ANSWERED:
      await db.quizAnswers.create({
        userId: user.id,
        questionId: activityId,
        score: stmt.result?.score?.raw,
        success: stmt.result?.success,
      });
      break;

    case VERB_PROGRESSED:
      const progress = stmt.result?.extensions?.[
        'https://w3id.org/xapi/video/extensions/progress'
      ];
      if (progress) {
        await db.lessonProgress.updateProgress(user.id, activityId, Number(progress));
      }
      break;
  }
}

Analytics via xAPI: The LRS accumulates rich data on learner behavior — detailed analytics can be built:

-- Average score per lesson
SELECT
  s.object->>'id' AS activity_id,
  s.object->'definition'->'name'->>'ru-RU' AS lesson_name,
  AVG((s.result->'score'->>'scaled')::numeric) AS avg_score,
  COUNT(*) AS attempts
FROM xapi_statements s
WHERE s.verb->>'id' = 'http://adlnet.gov/expapi/verbs/completed'
  AND s.result->'score' IS NOT NULL
GROUP BY 1, 2
ORDER BY avg_score;

Process and Timelines

  1. Analysis — study your current LMS, identify integration points.
  2. Design — develop LRS architecture, data model, API.
  3. Implementation — write LRS code, configure connectors.
  4. Testing — verify statement correctness, performance.
  5. Deployment — deploy to production, set up monitoring.

Timelines: basic integration — from 1 week, custom solution — 3–5 additional days. Investment in xAPI pays off within 6–9 months by reducing SCORM content maintenance costs by half.

What's Included in the Work

  • LRS development/setup
  • xAPI integration with your LMS
  • xAPI content creation (if required)
  • API and administration documentation
  • Team training on xAPI
  • Post-launch support

Our Experience

We have been developing LMS solutions for over 5 years. During this time, we have completed over 50 projects for EdTech companies and corporate universities. Among them: xAPI integration for simulator tracking, mobile learning with offline synchronization, custom LRS with real-time analytics. We guarantee support for all xAPI 1.0.3 versions and a certified solution.

Contact us to evaluate your project. Order LMS development with xAPI support — get a flexible tool to measure learning experience.

Backend Development Services: Laravel, Node.js, Go, Django, PostgreSQL

On a production server at 3:14 AM, the Laravel Jobs queue stopped processing. 40,000 unprocessed jobs in Redis. Cause: worker crashed due to a memory leak in one of the Jobs (leak via a static variable in an Eloquent observer), supervisor didn't restart it because of misconfigured stopwaitsecs. This is not a hypothetical scenario — it's Tuesday. We analyzed such an incident on a project with 500 RPS load: diagnosis took 4 hours, fix — 20 minutes. So you don't lose money on downtime, we offer backend development services with a focus on production-grade reliability. We'll assess your project in 2 days.

Backend is what works when no one is watching. Or doesn't work. We guarantee you'll have the first option.

How do we ensure production-grade reliability from day one?

What we do correctly from day one

Service Layer over Fat Controllers. Controller receives HTTP request, validates it via Form Request, passes data to Service, returns response. Business logic in Service, not Controller. This sounds trivial, but most legacy projects have controllers with 500 lines and SQL queries inside.

Repository Pattern we use cautiously. If you just wrap Model::where(...) in a repository method — that's boilerplate without benefit. Repository is justified when: you need to abstract from the data source (DB + cache + external API) or when query logic is complex enough to isolate.

Jobs, Events, Listeners. Everything that can be async — make async. Sending email, PDF generation, external API sync, aggregate recalculation — into Queue. Laravel Horizon for queue monitoring in Redis: see throughput, failed jobs, processing time per queue.

How Octane handles high load

Laravel Octane with RoadRunner or Swoole keeps the app in memory between requests — removes bootstrap overhead (config loading, class autoloading) on each HTTP request. Gain: 3–8x on synthetic benchmarks, 2–4x on real applications. Important: no state between requests in static variables — that leads to exactly the incidents from the beginning. We use this in projects with >1000 RPS.

What to do about N+1 queries

N+1 is the most common cause of slow pages in Laravel apps. Standard story: page worked fine on dev with 10 records, on production with 10,000 — 8-second load.

Laravel Debugbar in dev environment shows the number of queries per page. More than 20 queries per page — signal for audit.

Model::preventLazyLoading(! app()->isProduction());

Telescope for profiling in staging: logs all queries, jobs, mail, notifications with time detail. Numbers: after implementing eager loading, page load time drops from 8s to 0.3s — 27 times faster.

PostgreSQL: indexes that are actually needed

PostgreSQL 14+ is the primary DB on all projects. We use PgBouncer + PostgreSQL combination. 10+ years experience, more than 50 backend projects, 5 years on the market.

How PostgreSQL helps avoid slow queries

Composite indexes for frequent WHERE + ORDER BY. If you have WHERE user_id = ? AND status = ? ORDER BY created_at DESC — you need (user_id, status, created_at DESC). A separate index on (user_id) doesn't help much with sorting.

Partial indexes. If 95% of queries go with WHERE status = 'active':

CREATE INDEX idx_orders_active ON orders (created_at DESC)
WHERE status = 'active';

The index is small, fast, covers the main load.

GIN indexes for JSONB and arrays. @> operator without GIN index — seq scan. With index — fast even on millions of rows.

GIN for full-text search. to_tsvector + GIN instead of LIKE '%query%'. LIKE without index is always seq scan. With pg_trgm extension and gin_trgm_ops — supports LIKE with index, useful for CRM search by partial match.

Connection pooling: why it's more important than it seems

Rails, Laravel, Django open a new connection to PostgreSQL for each PHP/Python process. With 100 workers — 100 connections. PostgreSQL starts degrading from 200–300 active connections — overhead on connection management becomes significant.

PgBouncer — connection pooler in front of PostgreSQL. Transaction pooling mode: connection to PostgreSQL is occupied only during a transaction, returned to pool between requests. 1000 application workers → 20–50 actual connections to PostgreSQL. This reduces latency by 40% and hosting costs by 30%.

Node.js with Fastify: when it's better than Laravel

Node.js is justified for:

  • Realtime: WebSocket servers, Server-Sent Events, chat, live updates
  • Streaming: large files, video, streaming data
  • High I/O concurrency: many parallel requests to external APIs without heavy business logic
  • Serverless: Lambda/Cloud Functions — Node.js starts faster than PHP

Fastify over Express: 2–3 times faster on benchmarks, built-in JSON Schema validation, better TypeScript support, plugin architecture.

Typical realtime architecture: Laravel — core business logic and REST API. Node.js + Socket.io or ws — WebSocket server. Laravel publishes events to Redis Pub/Sub, Node.js subscribes and broadcasts to clients. This separation allows scaling the WebSocket server independently of the main app.

Go: microservices and high load

Go we use for:

  • High-load microservices (>10,000 RPS)
  • Background workers with strict latency requirements
  • DevOps tools and CLI
  • gRPC services in microservice architecture

Goroutines — thousands of times cheaper than OS threads. 10,000 concurrent connections on Go is normal on one server.

But Go is not a silver bullet. Development is slower than Laravel: more boilerplate, no ORM at Eloquent level, error handling with if err != nil everywhere. Justified only when performance is a real requirement, not an assumption.

Django and Python backend

Django with DRF (Django REST Framework) — for tasks where Python is needed: ML pipelines, data processing, integrations with AI tools.

Celery for background tasks — similar to Laravel Queue but more complex to configure. Celery Beat for cron tasks.

Django ORM vs raw SQL: ORM is convenient for CRUD. For analytical queries with multiple JOINs, window functions, and CTEs — connection.execute() with raw SQL is more readable and predictable.

Redis: not just cache

Redis in our projects plays multiple roles:

Role Details
Cache Caching results of heavy queries, HTML fragments
Queues Backend for Laravel Queue / Celery
Session store Distributed sessions in multi-instance environment
Pub/Sub Realtime events between services
Rate limiting Sliding window counters for API throttling
Leaderboards Sorted Sets for rankings

Redis Cluster for horizontal scaling. Sentinel for automatic failover on standalone setups.

Deployment and infrastructure

Docker + docker-compose — standard for local development and production. Each service in a container: PHP-FPM/Octane, Nginx, PostgreSQL, Redis, Queue Worker, Scheduler.

CI/CD via GitHub Actions:

  1. Run tests (PHPUnit / Pest, Vitest, Playwright)
  2. Build Docker image
  3. Push to Container Registry
  4. Deploy: docker pull → docker-compose up -d on server, or Kubernetes rolling update

Zero-downtime deploy for Laravel: php artisan down --secret=TOKEN is not needed with proper configuration. Strategy: new container starts next to the old one, Nginx switches traffic after health check, old container stops.

Monitoring: Sentry for exception tracking with alerting in Slack/Telegram. Grafana + Prometheus (or Grafana Cloud) for metrics: CPU, memory, request rate, queue depth, database connection count. Alerts on: error rate > 1%, p99 latency > 2s, queue depth > 1000 jobs.

What's included in turnkey work

  • Architecture design (API documentation, DB schema, service diagram)
  • Implementation according to agreed specification with code review
  • CI/CD, monitoring, alerting setup
  • Load testing (k6, wrk) with report
  • Handover of source code, access, deployment instructions
  • Training of customer's team (2-3 sessions)
  • Warranty support for 1 month after delivery

Timeline benchmarks

Task Timeline
REST API for mobile/SPA (medium complexity) 6–12 weeks
Backend with complex business logic + integrations 12–20 weeks
High-load service on Go 8–16 weeks
Migration from legacy PHP to Laravel 16–32 weeks

Pricing is calculated individually after analyzing load, integrations, and business logic. Contact us for a free audit of your current backend — get an optimization plan in 2 days. Request a consultation.