Local-first explorer for archived X (Twitter) posts and replies.
Import your official X export, index everything locally, search with filters, and export results.
No scraping as the primary strategy.
Production-oriented baseline implemented with:
- π Local admin authentication
- π Sign in with email or username
- π₯ Full user management (create/edit/delete/change password)
- π§ Role-based access control (
admin/viewer) - π‘οΈ Protected default user (cannot be deleted; password can be changed)
- π¦ Login rate limiting (IP-based temporary block after repeated failures)
- ποΈ Archive/project creation
- π¦ Official X archive import (
.zip/.json) - π Reimport modes:
mergeandoverwrite - π§© Parser/normalizer for posts, replies, hashtags, mentions, URLs, media
- ποΈ SQLite storage + FTS5 full-text search
- π Rich filters (date range, author, language, replies/media/links only)
- π§΅ Conversation view by
conversation_id - π€ CSV / JSON / HTML export
- π§° Async import queue with worker, retry, progress
- πΌοΈ Local media extraction/preview from archive ZIP
- π Optional sync via official X API (
username/user_id, incrementalsince_id) - π§ Adaptive API rate-limit handling per endpoint + retry/backoff
- π Advanced conversation reply pagination (
next_token, multiple pages per conversation) - β±οΈ Conversation reply time-window filter (
start_time, configurable in days) - π Operational logs + persisted operational metrics (
operation_metrics) - π©Ί Runtime probes:
/healthz(liveness) and/readyz(database readiness) - π§· Request correlation with
X-Request-IDon every response - π Default UI in Brazilian Portuguese with language switch (Portuguese/English)
- Backend:
Flask - Database:
SQLite+FTS5 - Frontend:
Jinja2+ CSS - Auth:
Flask-Login - Security:
Flask-WTF(CSRF), upload validation, upload size limits
x-archive-explorer/
ββ app/
ββ instance/
ββ uploads/
ββ migrations/
ββ tests/
ββ run.py
ββ start-x-archive-explorer.bat
ββ docker-compose.yml
Use the intelligent launcher:
start-x-archive-explorer.batWhat it does automatically:
- Creates
.venvif missing - Installs dependencies from
requirements.txt - Finds a free port between
5000and5100 - Reconciles legacy DB state (
db stamp headwhen needed) - Runs DB migrations (
flask db upgrade) - Prints current default login variables for local bootstrap
- Opens browser automatically
- Starts the app on the selected port using
waitress(production-grade WSGI server)
Default local bootstrap access:
- Username:
admin - Email:
admin@localhost - Password: generated securely at bootstrap when
XAE_ADMIN_PASSWORDis not set - In development, when
XAE_ADMIN_PASSWORDis not set, the launcher generates a strong bootstrap password automatically - Generated bootstrap password is saved to:
instance/bootstrap-admin-password.txt(restricted local permissions) - Sign in with either the username or the email.
Security recommendation:
- Change the default password immediately after first sign-in.
- Go to Users in the top navigation and update the password of the default account.
If the default password no longer works on an existing database, reset it with:
flask --app run.py reset-user-password --identity admin@localhost --password <new-password>If you upgraded code and the app reports outdated schema, run:
flask --app run.py db upgrade- Create and activate virtual environment.
- Install dependencies:
pip install -r requirements.txt- Apply migrations:
flask --app run.py db upgrade- Set environment variables:
set XAE_SECRET_KEY=your-strong-key
set XAE_ADMIN_USERNAME=admin
set XAE_ADMIN_EMAIL=admin@localhost
set XAE_ADMIN_PASSWORD=your-strong-password
set XAE_AUTO_CREATE_SCHEMA=false- Run:
python run.py- Open:
http://127.0.0.1:5000
docker compose up --buildServices:
x-archive-explorer(web, served by Gunicorn)x-archive-worker(queue worker)- Both containers run
flask db upgradeautomatically on startup before serving traffic.
Important:
- In production mode,
docker-compose.ymlrequiresXAE_SECRET_KEYandXAE_ADMIN_PASSWORD. - If these variables are missing, Docker Compose will fail fast at startup.
- Copy
.env.production.example(or.env.example) to.env.productionand set strong values:
XAE_ENV=production
XAE_SECRET_KEY=<64+ char random secret>
XAE_ADMIN_PASSWORD=<strong bootstrap password>
XAE_SECURE_COOKIES=true
XAE_TRUST_PROXY_HEADERS=true
XAE_ENABLE_HSTS=true- Start services with the production env file:
docker compose --env-file .env.production up -d --build- Verify runtime probes:
curl http://127.0.0.1:5000/healthz
curl http://127.0.0.1:5000/readyz- Sign in using:
- Username:
admin(or yourXAE_ADMIN_USERNAME) - Email:
admin@localhost(or yourXAE_ADMIN_EMAIL) - Password: value from
XAE_ADMIN_PASSWORD
- Immediately rotate the bootstrap password in Users after first login.
You have two supported paths:
- In X, request your account archive.
- Then import the
.zip/.jsonin X Archive Explorer.
To request archive in the X interface:
- English UI path: More > Settings and privacy > Your account > Download an archive of your data
- Portuguese UI path: Mais > ConfiguraΓ§Γ΅es e Privacidade > Sua Conta > FaΓ§a download de um arquivo com seus dados
Official references:
- https://help.x.com/en/managing-your-account/accessing-your-x-data
- https://help.x.com/en/managing-your-account/how-to-download-your-x-archive
- Enable API sync in env variables.
- Open archive detail.
- Click Sync API X.
- Provide
usernameoruser_id. - Optional incremental sync (
since_id). - Optional conversation reply collection with multiple pages per conversation.
- Optional reply time window control (in days).
Official API references:
- https://docs.x.com/x-api/posts/search/introduction
- https://docs.x.com/x-api/posts/search-recent-posts
- https://docs.x.com/x-api/users/get-posts
- https://docs.x.com/x-api/getting-started/pricing
Developer rules:
- Sign in
- Create archive/project
- Import official export (
mergeoroverwrite) - Search with filters
- Open details and conversation threads
- Export results
- Optionally sync with X API
admin: full access (archives import/sync, user management, retry operations)viewer: read-only operations (dashboard/search/archive details) + own password change- Default system account is protected and always preserved as administrator
- Default language: PortuguΓͺs (Brasil)
- Language switch available in the top bar
- English is supported as an alternate UI language
Run one queued job:
flask --app run.py run-import-worker --onceRun continuously:
flask --app run.py run-import-workerAuthenticated JSON endpoint:
/ops/metrics
Prune old metrics:
flask --app run.py prune-op-metrics --days 90- Set a strong
XAE_SECRET_KEYin production - The app refuses to boot in non-development with missing/weak
XAE_SECRET_KEY - Store
XAE_ADMIN_PASSWORDsecurely - Do not expose
instance/x_archive.dbpublicly - Run behind HTTPS reverse proxy (Nginx/Caddy)
- Enable trusted proxy headers only behind your own reverse proxy (
XAE_TRUST_PROXY_HEADERS=true) - Keep secure cookies enabled in production (
XAE_SECURE_COOKIES=true) - Enable HSTS in production (
XAE_ENABLE_HSTS=true) with TLS end-to-end - Use
X-Request-IDin client/proxy logs to correlate app requests end-to-end - Back up SQLite database regularly
pip install -r requirements-dev.txt
python -m pytest -qBefore pushing:
- Run
python -m pytest -qand confirm all tests pass. - Ensure
.envfiles are not committed (.env.exampleand.env.production.exampleare tracked templates). - Ensure
instance/anduploads/contain only.gitkeep(no local data dumps). - Verify production values for
XAE_SECRET_KEY,XAE_ADMIN_PASSWORD, and HTTPS/proxy settings. - Apply migrations in target environment:
flask --app run.py db upgrade.
XAE_IMPORT_ASYNC=true|falseXAE_IMPORT_MAX_RETRIES=2XAE_IMPORT_WORKER_SLEEP_SECONDS=2XAE_IMPORT_STALLED_JOB_MINUTES=30XAE_LOGIN_RATE_LIMIT_WINDOW_SECONDS=300XAE_LOGIN_RATE_LIMIT_MAX_ATTEMPTS=5XAE_LOGIN_RATE_LIMIT_BLOCK_SECONDS=300XAE_ARCHIVE_MEMBER_MAX_FILE_MB=25XAE_ARCHIVE_MAX_TOTAL_MB=200XAE_STORE_RAW_JSON=true|falseXAE_EXPORT_MAX_POSTS=5000XAE_MEDIA_MAX_FILE_MB=25XAE_MEDIA_MAX_TOTAL_MB=500XAE_MEDIA_MAX_FILES=5000XAE_USER_ITEMS_PER_PAGE=25XAE_ARCHIVE_DETAIL_ITEMS_PER_PAGE=20XAE_CONVERSATION_ITEMS_PER_PAGE=50XAE_ARCHIVE_FILTER_MAX_OPTIONS=300XAE_DASHBOARD_ARCHIVES_PER_PAGE=8XAE_DASHBOARD_CACHE_TTL_SECONDS=30XAE_ADMIN_USERNAME=adminXAE_ADMIN_EMAIL=admin@localhostXAE_ADMIN_PASSWORD=<set-strong-password>XAE_X_API_ENABLED=falseXAE_X_API_BASE_URL=https://api.x.com/2XAE_X_API_BEARER_TOKEN=...XAE_X_API_TIMEOUT_SECONDS=20XAE_X_API_MAX_RETRIES=2XAE_X_API_BACKOFF_SECONDS=1.0XAE_X_API_MAX_BACKOFF_SECONDS=10.0XAE_X_API_ADAPTIVE_RATE_LIMIT_ENABLED=true|falseXAE_X_API_RATE_LIMIT_MAX_WAIT_SECONDS=60XAE_OPS_METRICS_RETENTION_DAYS=90XAE_LOG_LEVEL=INFO|WARNING|ERRORXAE_ACCESS_LOG_ENABLED=true|falseXAE_REQUEST_ID_HEADER=X-Request-IDXAE_REQUEST_ID_ACCEPT_INCOMING=true|falseXAE_GUNICORN_WORKERS=2XAE_GUNICORN_THREADS=2XAE_GUNICORN_TIMEOUT=60XAE_SECURE_COOKIES=true|falseXAE_TRUST_PROXY_HEADERS=true|falseXAE_PROXY_FIX_X_FOR=1XAE_PROXY_FIX_X_PROTO=1XAE_PROXY_FIX_X_HOST=0XAE_PROXY_FIX_X_PORT=0XAE_PROXY_FIX_X_PREFIX=0XAE_FORCE_HTTPS=true|falseXAE_ENABLE_HSTS=true|falseXAE_HSTS_MAX_AGE_SECONDS=31536000XAE_HSTS_INCLUDE_SUBDOMAINS=true|falseXAE_HSTS_PRELOAD=true|falseXAE_PREFERRED_URL_SCHEME=https|httpXAE_PORT=5000(optional, app runtime port)
A lightweight local-first explorer for archived X posts and replies.
Import, search, filter, inspect, sync, and export with speed and clarity.
- Initial release.
Contributions are welcome. Please read CONTRIBUTING.md before opening a pull request.
This project is licensed under the MIT License. See LICENSE for details.