Deploy full-featured web apps in minutes. Auth, payments, admin, email, database — one import.
Project description
Tidepool
┌─────────────────────────────────────────────┐
│ build locally → deploy in 30s → live │
│ ↑ ↓ │
│ iterate auth · payments · email │
│ ↑ db · files · http │
│ └────────── <you>.tidepool.sh ──┘ │
└─────────────────────────────────────────────┘
Build and deploy full-featured web apps in minutes. Auth, payments, admin, email, file storage, database, background tasks — all from a single tp object.
Designed for AI agents and humans with CLI coding tools like Claude Code. The API is self-documenting (curl https://tidepool.sh/api), and every feature is available first and foremost via the command line.
Tools such as auth flows, Stripe payments, admin and database ORM, multi-page routing, file storage, email, a key-value database, background tasks, etc are all built and documented with AI agents in mind. These tools are available via a single tp object; they are meant to provide abstractions for infrastructure that is hard for an AI to set up (eg, third party subscriptions that take lots of clicks/config). We abstract these away so that the AI can move fast in terms of core site design, logic, and styling. Logging and API errors/warnings/tips are meant to be as detailed as possible so that an agent, on its own, can quickly understand and solve whatever problems come up.
Pods scale horizontally to 10+ replicas with shared Postgres/Redis/R2 — enough for a Substack clone at 20k–50k DAU or a Reddit-style site at 5k–15k DAU. The goal: a fairly advanced website up and running in an afternoon. No servers, no Docker, no AWS.
Quickstart
Install
pip install tidepool
Init a pod
tidepool init my-blog
cd my-blog
Creates a directory with a default main.py:
import tp
tp.page('/', '<h1>Hello from Tidepool!</h1>')
Develop locally
tidepool dev
# Pod runs at http://localhost:8000
The dev server replicates production pod behavior: file I/O goes to ./tp_data/files/, tp.db persists to a JSON file, tp.state is readable at ?format=json. Stripe, R2, and email are optional — the app runs without them.
Deploy
tidepool --url https://tidepool.sh register --email you@example.com
# verify email, then:
tidepool deploy
# Pod is live at https://my-blog.tidepool.sh in ~30 seconds
deploy auto-discovers source files and uploads tp_data/files/ (images, media) and tp_data/secrets.json automatically.
Push & Pull
Pull a live pod to develop locally — all state comes with it:
tidepool pull abc123
# Creates my-blog/ with source files + tp_data/ (db, secrets, files)
cd my-blog
tidepool dev
# Edit code, add data, test locally...
tidepool push # pushes everything back (hash remembered from pull)
pull downloads source files, tp.db → tp_data/db.json, tp.secrets → tp_data/secrets.json, and pod files → tp_data/files/. The dev server reads all of these natively — no conversion needed.
push auto-discovers source files (same as deploy) and uploads them along with tp_data/db.json (merge by default), tp_data/secrets.json, and all files in tp_data/files/. Pushing source files triggers a pod restart. Use -y to skip the confirmation prompt.
tidepool pull abc123 --dir . # pull into current directory instead of a subdirectory
tidepool push abc123 # explicit hash (overrides remembered hash)
tidepool push --file main.py # push specific files instead of auto-discover
tidepool push --secret STRIPE_KEY=sk_xxx # override a secret
tidepool push --replace-db # replace all db keys instead of merging
tidepool push --sync # delete remote files not present locally
tidepool push -y # skip confirmation prompt
.tpignore
Create a .tpignore file to exclude files from deploy and push (same syntax as .gitignore):
# Directories (trailing slash)
data/
notebooks/
# File patterns
*.csv
*.npy
*.pkl
*.parquet
pipeline.py
build_log.*
Always ignored regardless of .tpignore: tp_data/, __pycache__/, .git/, venv/, .venv/, node_modules/, dotfiles, and build_log.*. Files over 50MB are skipped automatically with a warning.
tidepool init generates a starter .tpignore. For existing projects, create one before your first deploy or push to avoid uploading data files, models, or build artifacts.
Eject Mode
For full control over the runtime, eject the internals into your project:
tidepool eject
# Copies tp_runtime.py, tp_server.py, tp_backend.py, tp_templates/ into your project
These files are now yours to modify. tidepool dev, deploy, and push auto-detect eject mode when tp_server.py exists in the project directory — no flags needed. To undo, delete the ejected files.
Runtime Tools
Use import tp at the top of every .py file. main.py runs once at startup to configure the pod — set auth, payments, seed data, register routes. The server dispatches requests directly to handlers.
| Name | Description | Usage |
|---|---|---|
tp.route |
Register a request handler with path params | @tp.route('/post/:slug', methods=['GET']) |
tp.page |
Register a static HTML page (no handler) | tp.page('/about', '<h1>About</h1>') |
tp.auth |
Full auth system. Presets: 'paywall' (pay-first + magic link) or 'standard' (email/password) |
tp.auth = 'paywall' or tp.auth = 'standard' |
tp.payments |
Stripe subscriptions and one-time purchases (in cents) | tp.payments = {products: [{id: 'pro', price: 500, recurring: 'month'}]} |
tp.admin |
Auto-generated admin panel at /_admin/ |
tp.admin = {users: ['admin@example.com']} |
tp.create_user |
Create user with hashed password (idempotent) | tp.create_user('sam@x.com', 'pass', subscriptions={'pro': True}) |
tp.db |
Key-value store, 1GB limit, persisted across runs | tp.db.set('post:slug', {...}) / tp.db.get('post:slug') |
tp.files |
File storage (R2 in prod, 50GB), served at /_files/ |
tp.files.write('photo.jpg', data) / tp.files.read('photo.jpg') |
tp.email |
Send email with optional HTML and attachments | tp.email('user@x.com', 'Subject', 'body', html='<p>hi</p>') |
tp.http |
HTTP client (same API as requests), 200 req/60s, SSRF-protected |
tp.http.post(url, json=payload, headers={...}) |
tp.markdown |
Convert markdown to HTML (tables, code, footnotes) | html = tp.markdown('# Hello\n\nWorld') |
tp.secrets |
Read-only dict of deploy-time credentials | api_key = tp.secrets['STRIPE_KEY'] |
tp.state |
Public app state dict, readable at ?format=json |
tp.state = {'status': 'live'} |
tp.publish |
Update public JSON state (ETag-supported polling) | tp.publish({'messages': msgs}) |
tp.background |
Background tasks (max 5). seconds<=0: once, >0: loop |
@tp.background(seconds=3600) |
Handler return values: str → 200 HTML, dict/list → 200 JSON, int → status code, tuple(body, status) → body + status, None → 303 redirect, generator → SSE stream.
Request object: Handler receives (req, **params). Attributes: req.path, req.method, req.query, req.user (dict or None), req.body (dict), req.files (dict of upload metadata — file data auto-saved to tp.files).
Auth details
Auth presets: tp.auth = 'paywall' for payment-first apps (no signup form, accounts auto-created at checkout, magic link for return logins — pair with tp.payments). tp.auth = 'standard' for traditional email/password signup with confirmation, reset, and magic link. Customize after setting a preset: tp.auth['required'] = ['/dash/*'], tp.auth['theme'] = {'accent': '#e74c3c'}. Or pass a full dict for manual control: tp.auth = {required: ['/dash/*'], signup: True, reset: True, oauth: ['google']}.
Email confirmation on by default — signup_confirm: False to disable. req.user in handlers gives the logged-in user including subscriptions, purchases, and avatar_url (from Google profile). Theme: theme: {page: '<html>...{content}...</html>'} wraps auth pages in your layout; {content} receives the form, {title} the page title. Simpler: theme: {accent: '#color', css: '...'}.
Google OAuth setup: Add google_client_id and google_client_secret to tp_data/secrets.json. Get credentials at Google Cloud Console → Create OAuth 2.0 Client ID (Web application). Add authorized redirect URI: http://localhost:8000/_auth/oauth/google/callback for dev, https://yourdomain.com/_auth/oauth/google/callback for prod. That's it — the server handles the rest.
Payments details
Set tp.payments = {products: [{id: 'pro', name: 'Pro', price: 500, recurring: 'month'}]}. Users pay at /_pay/pro, manage subscriptions at /_pay/portal. recurring: 'month'/'year' for subscriptions; omit for one-time. Dev mode simulates purchases instantly. Prod requires tidepool stripe-connect (one-time setup).
Admin details
Set tp.admin = {models: {post: {fields: {title: 'string', body: 'text', tier: 'choice:free,pro'}, display: ['title']}}}. Field types: string, text, bool, number, date, choice:a,b,c. Plus read-only views of users, payments, emails, files. If not set, auto-inferred from tp.db key patterns. Admin access control: with tp.auth configured, set users: ['admin@example.com'] to restrict to specific emails (otherwise any logged-in user can access). Without tp.auth, admin is open in dev and key-gated in prod (key printed in server logs at startup, access via /_admin?key=<key>).
Background tasks
@tp.background() # runs once at startup
def migrate(tp):
if not tp.db.get('_migrated_v2'):
for key, val in tp.db.prefix('post:'):
val['version'] = 2
tp.db.set(key, val)
tp.db.set('_migrated_v2', True)
@tp.background(seconds=3600) # every hour
def send_digest(tp):
for email, user in tp.users().items():
if user.get('subscriptions', {}).get('digest'):
posts = tp.db.prefix('post:', reverse=True, limit=5)
tp.email(email, 'Hourly Digest', '\n'.join(t for _, t in posts))
Server-Sent Events (SSE)
Return a generator from any route handler to stream real-time events:
@tp.route('/feed/live')
def live_feed(req):
def stream():
last_count = 0
while True:
messages = tp.db.prefix('msg:', reverse=True, limit=20)
if len(messages) != last_count:
last_count = len(messages)
yield {'messages': [m for _, m in messages]}
time.sleep(2)
return stream()
Client-side: new EventSource('/feed/live'). Max 100 concurrent SSE connections per pod.
Static files & templating
- Static files — Files in
static/alongsidemain.pyare served at/static/<path>. - Jinja2 — Pre-installed.
from jinja2 import Environment, FileSystemLoader; env = Environment(loader=FileSystemLoader('templates'), autoescape=True). Render:env.get_template('page.html').render(posts=p).
Full API reference
curl https://tidepool.sh/api
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tidepool-0.1.3.tar.gz.
File metadata
- Download URL: tidepool-0.1.3.tar.gz
- Upload date:
- Size: 54.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6ef7058dae097cb146d0a2787f81968f485fbb89cd4fd3f5ad4181eef9f37bb6
|
|
| MD5 |
f7540821bf43607f0aa7bc10cc4e8818
|
|
| BLAKE2b-256 |
602ffc823b6d90878a80c9ed83139827a3134f092782cecb4d2a28e76bb0af25
|
File details
Details for the file tidepool-0.1.3-py3-none-any.whl.
File metadata
- Download URL: tidepool-0.1.3-py3-none-any.whl
- Upload date:
- Size: 52.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
091817e7b46b1fdf8763be9964e92ccbe0eca7ce8110996089307263460ce74c
|
|
| MD5 |
0926a1efa436995567641d9fdf50411b
|
|
| BLAKE2b-256 |
141dbd22ad4b8b41e041639bd2a293dd00669b07d0cd84e3120ce60f12f697cd
|