TL;DR
YouViCo’s guest access model uses secure, expiring tokens instead of login credentials. Reviewers click a link, gain access to video projects, and leave comments—no account creation needed. Architecture: JWT tokens (signed, expiring), permission scopes (read-only vs. comment vs. edit), role-based access control (RBAC), and real-time revocation. This reduces collaboration friction while maintaining enterprise security.
The Problem
Collaboration tools often have painful access patterns:
- Internal teams: “Create an account, wait for invite email, click link, set password, download authenticator app…”
- External clients: Same friction, plus legal/security reviews
- Result: A 30-minute onboarding just to leave feedback on a video
YouViCo’s insight: Most reviewers don’t need accounts. They need:
- Access to one video project
- Permission to comment (or just view)
- A session that expires in a few days
Why build accounts for that?
Architecture: The Three Layers
Our guest access stack:
Layer 1: Token Generation (Server-side)
↓
Layer 2: Token Validation (API Gateway)
↓
Layer 3: Permission Enforcement (Service Level)
Layer 1: Token Generation
When a project owner wants to invite external reviewers, they click “Generate Guest Link”:
interface GuestAccessRequest {
projectId: string;
expiresIn: number; // hours until token expires
permissions: Permission[]; // ['view', 'comment', 'resolve-comments']
maxUses?: number; // optional: one-time link
}
class GuestAccessService {
generateToken(request: GuestAccessRequest): string {
const payload = {
sub: `guest:${uuid()}`, // Subject: anonymous guest
projectId: request.projectId,
permissions: request.permissions,
iat: now(),
exp: now() + (request.expiresIn * 3600), // Expiration time
maxUses: request.maxUses || unlimited
};
// Sign with server private key
const token = jwt.sign(payload, PRIVATE_KEY, {
algorithm: 'RS256'
});
// Generate shareable link
return `https://youvico.com/project/${request.projectId}?token=${token}`;
}
}
Key properties:
- Tokens are signed: Can’t be forged or modified
- Tokens expire: No perpetual access
- Tokens are project-scoped: Can’t access other projects
- Tokens have permissions: Reviewers can only comment, not delete
Layer 2: Token Validation (API Gateway)
Every request with a guest token hits the API gateway:
class GuestTokenMiddleware {
async validateToken(req: Request): Promise<GuestContext | null> {
const token = extractFromQuery(req, 'token') || extractFromHeader(req, 'Authorization');
if (!token) return null;
try {
// Verify signature and expiration
const decoded = jwt.verify(token, PUBLIC_KEY);
// Check if token has been revoked
const isRevoked = await redis.get(`revoked:${decoded.jti}`);
if (isRevoked) {
throw new Error('Token revoked');
}
// Check usage limit
if (decoded.maxUses) {
const usageCount = await redis.incr(`token:usage:${decoded.jti}`);
if (usageCount > decoded.maxUses) {
throw new Error('Token usage limit exceeded');
}
}
return {
guestId: decoded.sub,
projectId: decoded.projectId,
permissions: decoded.permissions,
expiresAt: new Date(decoded.exp * 1000)
};
} catch (err) {
return null;
}
}
}
Why Redis for revocation? Allows instant token revocation without waiting for JWT expiration.
Layer 3: Permission Enforcement
Each service enforces the guest’s permissions:
class CommentService {
async createComment(guestContext: GuestContext, input: CreateCommentInput) {
// Verify guest has 'comment' permission
if (!guestContext.permissions.includes('comment')) {
throw new Error('Insufficient permissions');
}
// Verify guest is accessing their own project
const project = await getProject(input.projectId);
if (project.id !== guestContext.projectId) {
throw new Error('Unauthorized project access');
}
// Create comment with guest attribution
return await Comment.create({
...input,
authorId: guestContext.guestId,
isGuest: true // Track as guest for analytics
});
}
}
class VideoService {
async getVideo(guestContext: GuestContext, videoId: string) {
// 'view' permission is implicit - if token is valid, guest can view
// (no need to check 'view' explicitly, presence of token = access)
const video = await getVideo(videoId);
// Verify guest can access this video (it's in their project)
if (video.projectId !== guestContext.projectId) {
throw new Error('Unauthorized access');
}
return video;
}
}
Permission Model
Guests get role-based permissions. Typical roles:
| Role | Permissions | Use Case |
|---|---|---|
| Viewer | view | Stakeholder review (read-only) |
| Commenter | view, comment | Client feedback (can comment but not resolve) |
| Approver | view, comment, resolve | Creative director (full review authority) |
Each permission is enforced at the service level.
Real-Time Revocation
What if you need to revoke access immediately (e.g., client relationship ended)?
class GuestAccessController {
async revokeToken(projectId: string, tokenJti: string) {
// Set revocation flag in Redis with expiration = token's original expiration
const token = await Token.findByJti(tokenJti);
const expirationSeconds = Math.floor((token.expiresAt - now()) / 1000);
await redis.setex(
`revoked:${tokenJti}`,
expirationSeconds,
'true'
);
// Revoked token becomes useless immediately
// No API calls will succeed with this token
return { success: true, message: 'Token revoked' };
}
}
Result: Guest loses access within seconds. No session cleanup needed.
URL Encoding & Security
Guest links are shared via email, Slack, text. They need to be:
- Short (shareable)
- Secure (no tampering)
- Expiring (limited lifetime)
We use:
https://youvico.com/project/abc123?token=eyJhbGc...
1. Project ID in URL path: Transparent, easy to understand
2. Token in query string: Cryptographically signed, can't be modified
3. Token signed with RS256: Client verifies with public key, can't forge
Security note: We strongly recommend HTTPS-only links. Tokens are bearer tokens; they should never travel over HTTP.
Analytics: Tracking Guest Access
Guests are anonymous by default, but we track their activity for analytics:
SELECT
token_id,
project_id,
permissions,
created_at,
expires_at,
comments_created,
videos_viewed,
last_accessed
FROM guest_access_logs
WHERE project_id = 'proj_xyz'
ORDER BY created_at DESC;
This helps project owners understand: “How many external reviewers? How engaged are they?”
Lessons Learned
-
Tokens > Passwords for one-time access. Passwords imply ongoing identity; tokens imply temporary permission.
-
Expiration is security: A token that expires in 7 days is safer than one that expires in 1 year. Balance convenience vs. security.
-
Redis revocation is instant: Don’t rely on JWT expiration alone for sensitive projects. Always have a revocation layer.
-
Scope is critical: A guest token should only work for one project, one IP range (optional), one user agent (optional). Overly broad tokens are security risks.
-
Audit logging: Track all guest access. If a security incident happens, you can see exactly what guests accessed.
Guest access transformed YouViCo from “account friction” to “one click to collaborate.” Adoption increased 3x when we launched this feature.