An interactive web Art Engine. The system combines persistent state, 3D interactive experience, and venue/user-level licensing to transform public screens into shared cultural memory spaces.
- App [MVP] -- Interactive Art Engine App
- Github Project Board - https://github.com/users/terencereilly/projects/6
- Project Overview
- Conceptual Framework
- MVP
- System Architecture
- Technical Summary
- About the Interactive Artwork
- Wireframe Design
- Core Features
- Backend Development
- Firestore
- Design
- Responsive Breakpoints
- Components & Layout Map
- Core Challenges & Solutions
- Testing
- Getting Started
- Future Enhancements
- Author
The Interactive Art Engine for Messages to the Future project enables:
- Licensing & Orchestration – Controlled deployment and versioning per venue.
- Persistent Interactive Artworks – Participatory Interactive experiences with isolated instances per user/ venue.
- Front-End Interactivity – React + Three.js rendering with live user submissions.
- Data & Analytics (Future) – Participation and engagement tracking.
- Multi-Tenant Architecture (Future) – Separate instances, permissions, and business logic per venue.
- Transform public screens into shared memory experiences
- Support collective authorship, reflection, and engagement
- Encourage time-based participation over passive consumption
MVP Functionality
- License an unique instance of an interactive artwork
- Apply version-specific logic and moderation
- Deploy artwork to venue screens
Future Functionality
- Engagement analytics dashboards
- Multi-artwork licensing
- AI-assisted moderation and reporting
I used Agile development with Epic Stories and used MOSCOW priorties to help build out the MVP. Here's the stage of the project so far:
Use cases:
- Any user can can invite family, friends to uniquely particpate in this shared experience over time.
- A Venue owner, organisation or brand can engage and invite their customers, visitors to particapte in their instance of this shared experience.
- Event Organisers can buy a license and invite attendees to have their own unique shared experiences before, during and after events.

Overview of user control with their artwork instances so far.

- Control Layer (Django) – Authentication, licensing, multi-tenant orchestration
- Memory Layer (Firestore) – Persistent state per instance, real-time updates
- Experience Layer (React + Three.js) – 3D rendering, user interactions, versioned logic
Backend: Django project with apps for artwork templates and licensed instances. Handles user authentication, instance creation, and serves dynamic templates.
Database: Uses Firestore for persistent storage of messages per artwork instance, with license-based Firestore rules.
Frontend: Artwork UI (React/Three.js) is embedded via iframe; uses FirestoreOlta.js for Firestore integration or browser localStorage for demo mode.
Integration: Instance data (license, collection ID, etc.) is passed from Django to the frontend via template-injected JavaScript.
Security: Firestore rules enforce write access only for valid, active licenses; demo mode never writes to Firestore.
Deployment: Frontend Interactive artworks are hosted on Vercel; backend runs locally and on Heroku.
graph TD
A[User / Venue] -->|Interacts| C[Frontend: React + Three.js]
C -->|Sends / Reads Data| B[Firestore: Persistent State]
C -->|Fetch Config| D[Django Backend: Control Layer]
D -->|Manages Instances & Permissions| B
D -->|API / Licensing| C
Shows the flow from user actions → 3D frontend → persistent Firestore state → Django orchestration layer.
What is it?
This is a time-based interactive artwork that invites people to leave short messages, reflections, or wishes for future generations.
Different Versions of the Artwork - [Future Plans]
**One artwork. Multiple creative systems. Each version contains different built-in logic, designed to suit different venues, audiences, and purposes — whether in a gallery, public space, school, festival, or cultural institution.
Some Artwork versions I will explore are ideas like:
- Time as a moral force
- Location-sensitive memory
- The future reshaping the past
- Weighted or evolving collective memory
How to participate in the artwork
- Click an empty panel to leave your message.
- Input name, location [Optional] - [Future Plans]
- Click a panel with a message to read more about the person who wrote it.
Below are the wireframe design for both the
- The Interactive Art Engine
- The persistent state interactive Artwork
+------------------------------------------------------+
| Interactive Art Engine |
|---|
| [Title] [About] [Dashboard] [Login/Register] |
| +------------------------------------------------------+ |
| Nav: |
| - Title: "Messages to the Future" |
| - Subtitle: "License & run your own copy of this |
| - evolving interactive artwork |
| - About [Artwork + Artist] |
| - About [Artwork + Artist] |
| +------------------------------------------------------+ |
| Browse Artwork Versions: |
| - Artwork Instance [A, B ...] |
| - Artwork image/preview [iframe with local storage] |
| - Get Instance [Own your own License] |
| - View Details / Edit |
| +------------------------------------------------------+ |
| Dashboard: |
| - User info |
| - List of users Artwork Instances + ID's |
| - License Start + End Dates |
| - End License Anytime [Deactivate License] |
| - Analytics summary [Number of Interactions] |
| +------------------------------------------------------+ |
| Responsive Layout: |
| - Mobile: Stacked panels, hamburger menu |
| - Tablet: Horizontal panels, collapsible sidebar |
| - Desktop: Full 3D canvas |
| +------------------------------------------------------+ |
| Footer: |
| - Links: About, Contact, GitHub |
| +------------------------------------------------------+ |
Here is the core user shared interaction
Panel selection in the 3D scene triggers a modal for text submission.

Expanding a panel reveals more about the message and the user.

-
Licensing & Orchestration
- Controlled deployment and versioning for each user
- License management for artwork instances.
- Each users get's their own unique interactive artwork instance. [Which has persistent state built into it]
-
Admin Dashboard
- User authentication and management.
- Overview of artwork instances and license status.
-
Artworks that remember everyone's interactions
- React + Three.js for immersive 3D rendering and user interaction.
- Real-time user contributions to collective Artwork
- User messages are stored persistently using Firestore database [Already integrated in MVP stage] & soon in the future via Arweave Protocol Link Text
-
Multi-Tenant Architecture (Planned/Future)
- Permissions, and business logic per venue.
-
Data & Analytics (Planned/Future)
- Participation and engagement tracking.
-
Responsive Interaction Design & user device detection
- Optimized layouts, design hook & interaction UX for desktop, tablet, and mobile devices. [Making the experience suitable for dfferent user devices and larger screens.]
-
Security
- Django checks Firestore rules for user access to update / evolve the interactive artworks.
- Django authentication for users and admins.
Django handles:
-
Authentication
-
Instance creation
-
License enforcement
-
Artwork Templates = master blueprint artworks (A, B ...)
-
Artwork Instance = licensed, isolated copy of artwork (A or B ...)
- User – Venue, Event, Organisation, Person
- Artwork – Master template for interactive experiences
- Artwork Version – Defines logic, rules, moderation
- ArtworkInstance – Isolated deployment per venue
- License – Start/end dates and Ownership status
- User: Each user can create 1 artwork instance of each artwork version.
- ArtworkTemplate: Defines a version/type of artwork (e.g., “Artwork 1A”).
- ArtworkInstance: Represents a user’s licensed copy of an artwork.
- Each instance is linked to one user and one artwork template.
- Each instance has a unique Firestore collection for its messages.
- Relationships:
ArtworkInstancehas a foreign key toUser(many-to-one).ArtworkInstancehas a foreign key toArtworkTemplate(many-to-one).
| Field | Description |
|---|---|
| id (PK) | Primary key |
| username | Username |
| Email address | |
| password | Hashed password |
| date_joined | Account creation date |
Represents a version/type of artwork (e.g., “Artwork 1A”).
| Field | Description |
|---|---|
| id (PK) | Primary key |
| name | Artwork name |
| version_code | Version code (e.g., v1a) |
| description | Description |
| created_at | Creation timestamp |
| is_active | Is template active? |
One template can have many user instances.
Represents a licensed copy owned by a specific user.
| Field | Description |
|---|---|
| id (PK) | Primary key |
| user_id (FK → User.id) | Foreign key to User |
| artwork_template_id (FK) | Foreign key to ArtworkTemplate |
| firestore_collection_name | Unique Firestore collection for messages |
| license_start_date | License start date |
| license_end_date | License end date |
| is_active | Is license active? |
| created_at | Instance creation timestamp |
User
└── 1 ────────────────┐
│
ArtworkInstance
│
ArtworkTemplate ── 1 ──┘
- An Artwork Template can generate many Artwork Instances.
- Each user can create only 1 Artwork Instance of each Artwork Template.
Here's how the unique constraint is implemented In Django:
UNIQUE (user_id, artwork_template_id)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['user', 'artwork_template'],
name='unique_user_template_instance'
)
]- firestore_collection_name = unique persistent message store Users license Artwork instances; each instance is based on a template and stores its messages in a unique Firestore collection.
Screen showing license validation for venues.

| Purpose | Color | Hex |
|---|---|---|
| Background | Dark Grey | #1a1a1a |
| Canvas | Darker Grey | #0d0d0d |
| Text | Light Grey | #e0e0e0 |
| Highlights | Medium Grey | #555555 |
| Controls | Soft Grey | #333333 |
Typography: system-ui, sans-serif — headings bold, body 0.9rem
| Device | Width | Notes |
|---|---|---|
| Mobile | 0–425px | Stacked layout |
| Tablet | 768–1024px | Horizontal panels |
| Desktop | 1024-2560px | Full 3D canvas |
Wireframes showing the app at various screen widths:
On mobile phone devices - width 425px:
|
On bigger mobile phone devices - Min width 768px:
|
On laptops / Desktop Computers - Min width 1024px:

On large Desktop Computers / TV screens / Projectors - Min width 2560px:

| Component | Description |
|---|---|
| Title Overlay | Intro title to help onboarding |
| Canvas Full | 3D tunnel, wall cells, MapControls |
| Top-Right Controls | Reset camera, toggle day/night |
| Contributions Panel | Shows submitted messages |
| Input Modal | Cell click → input modal |
| Expanded Message Overlay | Fullscreen message view |
| Close Button | Closes overlay |
Below are the major architectural challenges I faced and how they were fixed.
The Problem
- All artworks wrote to a single messages collection.
- Demo browsing mutated production data.
- No separation between instances.
- No separation between demo and licensed modes.
- Increased quota usage.
- No safe testing environment.
The Solution
A. Instance-Level Isolation
- Dynamic collection names via Django:
?collection=messages_<uuid> - One collection per licensed instance.
B. Demo / Production - Level Isolation
- Storage abstraction layer (
dbApi) switches between backends:- Demo →
localStorage(no persistent writes) - Licensed → Firestore (persistent writes)
- Demo →
- No Firestore access without collection param.
The Over-Engineered Approach
- Originally, Django mediated every user interaction: each frontend action had to go through the backend to check accounts, validate licenses, and then trigger Firestore writes.
- This added complexity, latency, and maintenance overhead, even though Django only needed to track
ArtworkInstancecreation and license flags.
The Simplified Solution
- Frontend now writes directly to Firestore.
- License state (
licenseValid,expiresAt) is injected from Django and included with each write. - Firestore security rules enforce license validity automatically:
licenseValid == trueexpiresAt > request.time
- Django remains the source of truth for:
- Creating and updating
ArtworkInstancerecords - Managing license metadata
- Creating and updating
- No backend round-trips are needed for routine interactions.
Outcome
- Lightweight, scalable architecture
- Clear separation of responsibilities: frontend handles interactions, backend handles license authority
- Safe, persistent, and venue-ready artwork licensing
The Problem
Firebase Admin credentials stored locally could not be accessed in production (Heroku).
The Solution
- Firebase service account JSON stored as a Heroku config variable
- Written to file at runtime
- Firebase Admin SDK initialized from that path
This enabled:
- Secure cloud deployment
- No committed credentials
- Environment-based configuration
The Problem
Users should only be able to create one active instance per artwork template.
The Solution
- A database-level constraint:
UNIQUE (user_id, artwork_template_id)
This ensures:
- One licensed instance per user per template
- Clean renewal logic
- No duplicate collection ownership
The Problem
Improper cleanup of Firestore onSnapshot() listeners caused:
- Duplicate data events
- Increased read usage
- Performance degradation
The Solution
- Proper
useEffectcleanup - Listener unsubscription on component unmount
- Single listener per collection instance
This reduced quota waste and stabilized realtime updates.
The system grew through four stages:
- Shared-state prototype – all users and artworks wrote to the same collection.
- URL-scoped instance isolation – each artwork instance got a unique Firestore collection.
- Backend-governed licensing – Django manages license state, ensuring only valid instances can write.
- Production-ready cloud deployment – safe, multi-tenant, scalable environment.
Now, the system is:
- License-aware – only valid licensed instances can persist data.
- Multi-tenant – supports multiple users and artworks independently.
- Instance-scoped – each artwork instance has its own isolated data.
- Realtime interactive artwork engine – frontend writes directly to Firestore for fast, live interactions.
- Django unit tests for models & business logic for licensing
-
This project includes Django unit tests for both model logic and Firestore integration. These tests ensure that your licensing, instance creation, and Firestore message storage all work as expected.
-
Example Firestore Integration Tests in Artwork App (tests.py):
from django.test import TestCase
# FirestoreIntegrationTests
# To create a new collection and one document in Firestore, then read it back and verify the content.
import os
import firebase_admin
from firebase_admin import credentials, firestore
from dotenv import load_dotenv
from django.test import TestCaseclass FirestoreIntegrationTest(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
load_dotenv()
FIREBASE_CRED_PATH = os.getenv("FIREBASE_CRED_PATH")
if FIREBASE_CRED_PATH and not firebase_admin._apps:
cred = credentials.Certificate(FIREBASE_CRED_PATH)
firebase_admin.initialize_app(cred)
cls.firestore_client = firestore.client()
def test_create_new_collection(self):
# Create a new collection and add a document
new_collection = "new_test_collection"
doc_id = "first_doc"
doc_ref = self.firestore_client.collection(new_collection).document(doc_id)
doc_data = {"message": "This is a new collection!", "user": "tester"}
doc_ref.set(doc_data)
# Read the document back
doc = doc_ref.get()
data = doc.to_dict()
self.assertIsNotNone(data)
self.assertEqual(data["message"], "This is a new collection!")
self.assertEqual(data["user"], "tester")
def test_firestore_write_and_read(self):
doc_ref = self.firestore_client.collection("test_collection").document("test_doc")
test_message = "Hello Terence from Django TestCase!"
doc_ref.set({"message": test_message})
doc = doc_ref.get()
data = doc.to_dict()
self.assertIsNotNone(data)
self.assertEqual(data.get("message"), test_message) def test_firestore_second_user_message(self):
# Ensure the first message exists
doc_ref1 = self.firestore_client.collection("test_collection").document("test_doc")
doc_ref1.set({"message": "Hello Terence from Django TestCase!"})
# Add a second message as another user
doc_ref2 = self.firestore_client.collection("test_collection").document("test_doc_2")
second_message = "Hello from a second user!"
doc_ref2.set({"message": second_message, "user": "user2"})
# Read all messages in the collection
docs = list(self.firestore_client.collection("test_collection").stream())
messages = {doc.id: doc.to_dict() for doc in docs}
self.assertIn("test_doc", messages)
self.assertIn("test_doc_2", messages)
self.assertEqual(messages["test_doc_2"]["message"], second_message)
self.assertEqual(messages["test_doc_2"]["user"], "user2")Performance and accessibility test results for the app.

- Python 3.11+
- Node.js 18+
- Firebase project for Firestore
- Modern browser
git clone https://github.com/terencereilly/interactive-art-engine.git
cd interactive-art-engineBackend
cd backend
pip install -r requirements.txt
python manage.py migrate
python manage.py runserverFrontend
cd ../frontend
npm install
npm startConfigure .env for API keys, database credentials, and environment variables.
- Multi-artwork licensing Engine
- Payment System [Stripe]
- Engagement analytics dashboard
- AI-assisted moderation
- Multi-language support
- Mobile deployment optimization
Terence Reilly
- GitHub: @terencereilly
- Email: terryreillyo@gmail.com





