Developer Resources -- The Allow2 MCP Server provides comprehensive API documentation, integration guides, architecture overviews, and interactive examples. Connect it to your AI coding assistant for the best development experience. Start there.
Server-side SDK for integrating Allow2 Parental Freedom into websites and web applications.
This is a Service SDK -- it runs on your server, not on the child's device. If you are building a device or product owned by a family (a game, smart device, desktop app, browser extension, etc.), you need the Device SDK instead.
Each user account on your service maps to exactly one Allow2 child identity via OAuth2.
- Node.js 18+ (uses native
fetch) - No external dependencies
npm install allow2-servicevar Allow2Client = require('allow2-service');
var MemoryTokenStorage = require('allow2-service/storage/MemoryTokenStorage');
var client = new Allow2Client({
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
tokenStorage: new MemoryTokenStorage(),
});For production use, replace MemoryTokenStorage with FileTokenStorage or your own implementation of TokenStorageInterface (e.g., database-backed).
Each user account on your service is linked to one Allow2 child identity. The parent authorizes the connection through the OAuth2 flow.
// Generate the authorization URL
var url = client.getAuthorizeUrl(userId, 'https://yourapp.com/callback', 'csrf-state');
// Redirect the parent to this URL
res.redirect(url);app.get('/callback', async function (req, res) {
var code = req.query.code;
var userId = getCurrentUserId(req);
var tokens = await client.exchangeCode(userId, code, 'https://yourapp.com/callback');
// Tokens are automatically stored. The user is now paired.
res.redirect('/dashboard');
});var paired = await client.isPaired(userId);
if (!paired) {
// Show "Connect with Allow2" button
}await client.unpair(userId);Check whether a child is allowed to use your service right now.
var result = await client.check(userId, [1, 3], 'Australia/Brisbane');
if (result.allowed) {
// Child is allowed -- serve content
} else {
// Child is blocked -- show block page
}var result = await client.check(userId, [
{ id: 1, log: true }, // Internet
{ id: 3, log: false }, // Gaming (don't log usage)
], 'Australia/Brisbane');
// Check specific activities
var internet = result.getActivity(1);
if (internet && internet.allowed) {
console.log('Internet allowed, ' + internet.remaining + ' seconds left');
}
// Day type info
if (result.todayDayType) {
console.log('Today is: ' + result.todayDayType.name);
}var allowed = await client.isAllowed(userId, [1], 'Australia/Brisbane');
if (!allowed) {
return res.status(403).send('Access restricted by Allow2 Parental Freedom');
}Three input formats are supported:
// Array of objects (most explicit)
[{ id: 1, log: true }, { id: 3, log: false }]
// Array of IDs (all logged by default)
[1, 3, 8]
// Object map (all logged by default)
{ 1: 1, 3: 1, 8: 1 }Children can request changes when they are blocked.
var result = await client.requestMoreTime(userId, 3, 30); // 30 more minutes of Gaming
console.log('Request ID: ' + result.requestId);
console.log('Status: ' + result.status); // 'pending'var result = await client.requestDayTypeChange(userId, 5); // Request Holiday day typevar result = await client.requestBanLift(userId, 3); // Lift Gaming banvar status = await client.getRequestStatus(result.requestId, result.statusSecret);
console.log(status); // { status: 'approved' } or { status: 'denied' }When the parent has no internet, the child can still get approval via voice codes. The child reads a challenge code to the parent (phone call, in person, etc.), the parent enters it in the Allow2 app (works offline), and reads back the response.
var VoiceCode = require('allow2-service/VoiceCode');
var RequestType = require('allow2-service/models/RequestType').RequestType;
// Generate a challenge-response pair
var pair = client.generateVoiceChallenge(
childSecret, // shared secret for this child
RequestType.MORE_TIME, // request type
3, // activity ID (Gaming)
30 // 30 minutes
);
console.log('Challenge: ' + pair.challenge); // e.g., "0 03 06 42"
console.log('Expected: ' + pair.expectedResponse); // e.g., "847291"
// Later, verify the parent's response
var valid = client.verifyVoiceResponse(childSecret, pair.challenge, parentResponse);
if (valid) {
// Apply the requested change
}The challenge is T A MM NN where:
- T = request type (0=more time, 1=day type, 2=ban lift)
- A = activity ID (zero-padded to 2 digits)
- MM = 5-minute blocks of time requested
- NN = random nonce
Children and parents can submit feedback directly to you, the developer.
var FeedbackCategory = require('allow2-service/models/FeedbackCategory');
await client.submitFeedback(userId, FeedbackCategory.BUG, 'The block page shows the wrong time remaining');var discussions = await client.loadFeedback(userId);
await client.replyToFeedback(userId, discussions[0].id, 'Thanks, we are looking into it.');Allow2Client (facade)
|
+-- OAuth2Manager -- Token exchange, refresh, pairing
+-- PermissionChecker -- /serviceapi/check with caching
+-- RequestManager -- Two-step request creation, status polling
+-- VoiceCode -- HMAC-SHA256 challenge-response
+-- FeedbackManager -- Submit, load, reply
|
+-- HttpClient -- Native fetch wrapper (replaceable)
+-- TokenStorageInterface -- Per-user token persistence (you implement)
+-- CacheInterface -- Permission check caching (you implement)
- MemoryTokenStorage -- In-memory Map. Good for testing.
- FileTokenStorage -- JSON files on disk with restrictive permissions.
Implement TokenStorageInterface:
var TokenStorageInterface = require('allow2-service/TokenStorageInterface');
class DatabaseTokenStorage extends TokenStorageInterface {
async store(userId, tokens) {
await db.query('INSERT INTO allow2_tokens (user_id, data) VALUES (?, ?) ON DUPLICATE KEY UPDATE data = ?',
[userId, JSON.stringify(tokens.toObject()), JSON.stringify(tokens.toObject())]);
}
async retrieve(userId) {
var row = await db.query('SELECT data FROM allow2_tokens WHERE user_id = ?', [userId]);
if (!row) return null;
var OAuthTokens = require('allow2-service/models/OAuthTokens');
return OAuthTokens.fromObject(JSON.parse(row.data));
}
async delete(userId) {
await db.query('DELETE FROM allow2_tokens WHERE user_id = ?', [userId]);
}
async exists(userId) {
var row = await db.query('SELECT 1 FROM allow2_tokens WHERE user_id = ?', [userId]);
return !!row;
}
}- MemoryCache -- In-memory Map with TTL. Good for single-process apps.
- FileCache -- Per-key JSON files with TTL.
Implement CacheInterface:
var CacheInterface = require('allow2-service/CacheInterface');
class RedisCache extends CacheInterface {
async get(key) {
var val = await redis.get(key);
return val ? JSON.parse(val) : null;
}
async set(key, value, ttl) {
await redis.setex(key, ttl, JSON.stringify(value));
}
async delete(key) {
await redis.del(key);
}
}var UnpairedError = require('allow2-service/exceptions/UnpairedError');
var TokenExpiredError = require('allow2-service/exceptions/TokenExpiredError');
var ApiError = require('allow2-service/exceptions/ApiError');
try {
var result = await client.check(userId, [1], 'Australia/Brisbane');
} catch (err) {
if (err instanceof UnpairedError) {
// User needs to re-pair via OAuth2
res.redirect(client.getAuthorizeUrl(userId, redirectUri));
} else if (err instanceof TokenExpiredError) {
// Refresh failed -- re-pair needed
res.redirect(client.getAuthorizeUrl(userId, redirectUri));
} else if (err instanceof ApiError) {
// API returned an error
console.error('API error:', err.httpStatusCode, err.responseBody);
}
}var client = new Allow2Client({
clientId: 'your-client-id', // Required
clientSecret: 'your-client-secret', // Required
tokenStorage: storage, // Required -- TokenStorageInterface implementation
cache: new MemoryCache(), // Optional -- defaults to MemoryCache
httpClient: customHttpClient, // Optional -- defaults to built-in fetch wrapper
apiHost: 'https://api.allow2.com', // Optional -- default
serviceHost: 'https://service.allow2.com', // Optional -- default
cacheTtl: 60, // Optional -- cache TTL in seconds (default 60)
});See LICENSE file.