Every family has a box of old photographs — grandparents in black and white, faded portraits from the 1950s, moments frozen in time that feel distant because the medium itself is foreign to modern eyes. The premise behind JBA Agency's photo animation tool is simple: use AI to close that distance. Upload a still image, and the AI generates a subtle, realistic animation — a slight head turn, a gentle breath, eyes that blink — transforming a photograph into something that feels alive.
This is not an acquired client project. This is JBA Agency's own product — built internally, dogfooding our own SaaS development capability, and deployed publicly at jbagency.ro/animate/. It serves as both a real utility for users and a live proof of concept for prospective SaaS development clients who want to understand what we can build for them.
"Building your own product is the most honest demonstration of your development capability. We didn't just claim we could build AI SaaS products — we built one, shipped it, and let anyone use it to verify the claim." — Alex, Founder, JBA Agency
The tool supports two use cases: animating still photographs (adding lifelike motion to static images) and colorizing black-and-white photos with AI-generated color. Both capabilities run through the same pipeline, with different parameters passed to the underlying AI model.
Want to see it in action? Try the live AI photo animation tool — free for your first generation.
Try the Animator FreeFor a SaaS built around AI API calls, Node.js's non-blocking I/O model is a natural fit. AI generation requests — particularly video generation from still images — are slow operations: anywhere from 8 to 45 seconds depending on model load. Node.js handles these as async operations without tying up server threads, meaning hundreds of concurrent requests can be in-flight while each waits for the AI API to respond.
Express was chosen over alternatives like Fastify or Koa because of ecosystem maturity —
the middleware for security (helmet), file uploads (multer), rate
limiting (express-rate-limit), and CORS handling all integrate cleanly with Express
and are well-documented. For a SaaS MVP that needs to be production-ready quickly, betting on
the most widely-understood framework reduces maintenance risk.
The data model for this SaaS has clear relational structure: users have credit balances, credit balances change via Stripe payment events, and each animation generation deducts from a user's balance and creates a history record. These are ACID-requiring transactions. A NoSQL database would introduce consistency risks — the scenario where a generation completes but the credit deduction fails, or a payment webhook is processed twice, would be catastrophic for a billing-critical system. PostgreSQL's transactional guarantees eliminate these risks.
The animation quality of an AI photo tool lives or dies on the underlying model. Grok AI's video generation API — specifically its image-to-video capability — produces realistic, temporally consistent motion from still photographs. The model understands facial geometry, lighting direction, and natural motion patterns, generating animations that feel physically plausible rather than jittery or uncanny.
Critically, Grok's API pricing model is per-generation, which aligns cleanly with a freemium SaaS business model: the cost of an API call is known in advance, enabling precise margin calculation for each tier. There are no unpredictable token costs that could erode margins at scale.
The key architectural principle: the credit deduction and the AI API call are coupled inside a single database transaction. If the Grok API call fails, the transaction rolls back — the user keeps their credit. If the API succeeds but the database write fails (unlikely, but possible), the transaction also rolls back. No credit is lost, no generation is lost, and no charge occurs without delivery.
Multer is configured with strict constraints before a file ever reaches the route handler:
const upload = multer({
storage: multer.memoryStorage(), // never write to disk unvalidated
limits: {
fileSize: 10 * 1024 * 1024, // 10MB max
files: 1 // single file per request
},
fileFilter: (req, file, cb) => {
const allowed = ['image/jpeg', 'image/png', 'image/webp'];
if (!allowed.includes(file.mimetype)) {
return cb(new Error('Only JPEG, PNG, and WebP images are accepted'));
}
cb(null, true);
}
});
Images are held in memory (not written to disk) during validation and then streamed directly to the Grok API. This eliminates an entire class of file system security risks and avoids the operational overhead of managing a temporary file directory with cleanup jobs.
Stripe powers the credit purchase flow. Users buy credit packs — not subscriptions — which matches the use case: someone who wants to animate a dozen family photos will buy a credit pack, use it, and may not return for months. A subscription model would create churn friction. Per-purchase credits create a frictionless "pay when you need it" experience.
POST /api/payments/create-checkoutmetadata: { userId, creditsPurchased }checkout.session.completed webhook event to our serverstripe.webhooks.constructEvent()credits = credits + purchased_amount WHERE user_id = ?"Never trust the client for payment confirmation. A user closing the browser tab, losing connectivity, or manipulating redirect parameters can all cause a redirect-based payment confirmation to fail silently. Webhooks from Stripe's servers, verified by signature, are the only reliable source of truth for payment events." — Alex, JBA Agency
The webhook endpoint uses raw body parsing (not JSON-parsed) to preserve the exact bytes for signature verification. Stripe signs webhooks with a secret; if the computed HMAC doesn't match, the request is rejected with a 400 status before any business logic runs. This prevents spoofed payment confirmations from an attacker who knows the endpoint URL.
// Webhook signature verification
app.post('/api/payments/webhook',
express.raw({ type: 'application/json' }), // raw body required
async (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body, sig, process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
return res.status(400).send(`Webhook error: ${err.message}`);
}
// handle verified event...
}
);
Stripe may deliver the same webhook event more than once — network failures, retries, and
edge cases can all cause duplication. The webhook handler checks the Stripe event ID against
a processed_events table before applying any credit changes. If the event ID
already exists, the handler returns 200 (acknowledging receipt) without reprocessing. This
ensures a user never receives double credits from a single payment, regardless of how many
times Stripe retries delivery.
Every response from the API server includes a hardened set of HTTP security headers via Helmet.js. Key headers include:
Content-Security-Policy — restricts which scripts, styles, and resources can loadX-Content-Type-Options: nosniff — prevents MIME type sniffing attacksX-Frame-Options: DENY — prevents clickjacking via iframe embeddingStrict-Transport-Security — enforces HTTPS for 1 year, including subdomainsReferrer-Policy: same-origin — prevents leaking referrer information to third partiesThe animate endpoint has two independent rate limiters applied in sequence:
The user-based limit is stored in PostgreSQL rather than in-memory (Redis), meaning it persists across server restarts and works correctly in a multi-instance deployment scenario.
The API's CORS policy uses a strict origin whitelist. Only requests originating from
jbagency.ro and www.jbagency.ro are accepted. This prevents
other websites from making authenticated cross-origin requests to the API on behalf of
a logged-in user (CSRF via third-party sites). Preflight OPTIONS requests
are handled explicitly and return the correct headers without invoking the auth middleware.
The pricing model is deliberately simple. Users get one free animation on account creation — enough to experience the product and judge quality — and then purchase credits to continue.
1 animation credit on signup. No credit card required. No time limit on using the free credit. This is not a trial — it's a permanent free tier with a single credit. The goal is to let users experience genuine value before any payment decision.
Credits purchased in packs. No subscription, no auto-renewal, no hidden fees. Credits do not expire. Each credit produces one high-quality animated video (up to 8 seconds) or one colorized photo. Premium animations run at higher resolution and longer duration than the free tier.
"Per-generation pricing aligns incentives perfectly. We only earn when a user gets value. There's no subscription to cancel, no annual lock-in. If the product is good, people buy more credits. If it's not, they don't. It's the most honest SaaS pricing model there is." — Alex, JBA Agency
Each animation generation has a known cost: the Grok API call cost plus a fraction of server compute. At $1.50 per credit with the AI API cost well below that figure, the margin per generation is healthy — particularly because infrastructure costs are fixed and margin improves with volume. The free tier credit costs approximately the same per-generation as a paid credit but is limited to one per user, so it functions as a capped acquisition cost rather than an ongoing loss.
AI video generation takes between 8 and 45 seconds. Showing a user a loading spinner for
45 seconds without feedback is terrible UX — they'll assume the system crashed. We implemented
a polling-based progress system: the generation request returns a job ID immediately, and the
frontend polls /api/animate/status/:jobId every 3 seconds, receiving a progress
percentage and estimated time remaining. Users see genuine progress, not a spinner, which
dramatically improved perceived performance and reduced abandonment during generation.
Early testing revealed that users uploading very large, high-resolution images (20MB+ DSLR
photos) created generation times at the extreme end of the range without proportional quality
gains — the AI model operates at its native resolution regardless of input resolution above
a certain threshold. We added a server-side image downscaling step (using the sharp
library) that resizes inputs above 4096px on the longest dimension before passing them to the
API. This reduced average generation time by 31% with no perceptible quality loss.
AI APIs are not 100% reliable. The Grok API occasionally returns 500 errors, particularly under high load. The system implements exponential backoff retry logic: on a 500 or 503 response, the handler waits 2 seconds, then 4 seconds, then 8 seconds before failing permanently. On permanent failure, the transaction is rolled back (user keeps their credit), and the user receives an error message explaining the generation failed with their credit preserved. This transparency — "we tried, it failed, you haven't been charged" — is critical for user trust in a paid service.
The AI model performs best on portrait photographs with clear subject-background separation. Group photos, photos with complex backgrounds, and very old daguerreotypes with significant damage produce inconsistent results. Rather than hide this, we added type-specific guidance on the upload page: a short checklist of photo characteristics that produce the best results, and a note on photo types where results may vary. This sets honest expectations and reduces support requests from users who uploaded a 1910 group photograph expecting Hollywood-quality animation.
The animate tool is live and used daily. Early user data shows:
Beyond the metrics, this project demonstrates something specific about JBA Agency's SaaS development capability: we built a complete, production-grade AI SaaS product — with payments, security, database design, AI integration, and a freemium business model — as an internal product, not as a theoretical exercise. Clients who want to build AI-powered SaaS products can see exactly what we'd build for them by using what we built for ourselves.
"The best portfolio piece is a live product with real users and real payments. You can't fake a working Stripe integration or a 99.6% uptime record. Ship something real." — Alex, JBA Agency
If you're evaluating JBA Agency for a SaaS development project, the animate tool is the most direct proof of capability we can offer. Try it at jbagency.ro/animate/. For the full scope of what we build — from SaaS platforms to AI integrations to 24-hour website delivery — see the about page. For AI strategy at the organizational level, explore the Fractional AI Officer service. Questions? Check the FAQ.
The AI photo animation tool is live. Try it free — no credit card required for your first generation.
Try It Now →