Skip to content

Database Schema

All tables are prefixed (tashil_ by default) and fully configurable via config/tashil.php under database.prefix and database.tables. A single migration creates them all. BaseModel resolves each table name and connection from config at runtime, so renaming a table is a config change, not a code change.

Relationships

packages ──┬─< package_feature >── features
           ├─< subscriptions ──┬─< subscription_features >── features
           │                   ├─< feature_usages >──────── features
           │                   ├─< usage_logs >──────────── features
           │                   ├─< subscription_events
           │                   └─< invoices ──< transactions
           └ (pending_package_id) ─ subscriptions

The aggregate root is subscriptions. Around it sit the immutable snapshot, the mutable counter, the audit log, the event store, and the invoices — each with a clear, single responsibility.

tashil_packages

The catalog of subscribable plans — pricing, trial config, and billing cadence.

ColumnTypeNotes
idBIGINT PK
slugVARCHAR UNIQUEStable identifier used by builders and lookups.
name / descriptionVARCHAR / TEXTDisplay fields.
price / original_priceDECIMAL(10,2)original_price is an optional strike-through.
currencyCHAR(3)ISO 4217; defaults to tashil.currency.
billing_periodVARCHARPeriod enum: day, week, month, year, lifetime.
billing_intervalINTMultiplier (e.g. month × 3 = quarterly).
trial_daysINTTrial length; 0 disables.
requires_paymentBOOLWhen true (and price > 0), the plan gates behind its first paid invoice. Authoritative at runtime; seeded from billing.activate_on_payment at creation.
is_active / is_featuredBOOL
sort_orderINT
metadataJSONYour custom data.
created_at / updated_at / deleted_atTIMESTAMPSoft-deletes enabled.

tashil_features

Feature definitions, independent of any plan.

ColumnTypeNotes
idBIGINT PK
slugVARCHAR UNIQUEStable identifier used in code.
name / descriptionVARCHAR / TEXT
typeVARCHARFeatureType enum: boolean, limit, consumable, enum, metered.
reset_periodVARCHARResetPeriod enum: never, daily, weekly, monthly, yearly.
is_activeBOOLGlobal kill-switch — when false, access checks refuse globally.
sort_orderINT
metadataJSON
created_at / updated_at / deleted_atTIMESTAMPSoft-deletes enabled.

tashil_package_feature (pivot)

Per-plan feature configuration.

ColumnTypeNotes
package_id / feature_idBIGINT FKCascade on delete. Unique together.
valueVARCHARLimit amount, boolean/enum string, or — for metered — the unit price.
is_availableBOOLIf false, the feature isn't synced on subscribe.
sort_orderINT

tashil_subscriptions (aggregate root)

The current state of one subscription. Updated on every transition; the immutable history lives in the event store.

ColumnTypeNotes
idBIGINT PK
subscriber_type / subscriber_idmorphsPolymorphic — any HasSubscriptions model.
package_idBIGINT FKCurrent plan.
pending_package_id / pending_change_atBIGINT FK / TIMESTAMPA scheduled change (e.g. downgrade at period end).
statusVARCHARSubscriptionStatus enum.
starts_at / ends_atTIMESTAMPFirst activation; lifetime cutoff (null for open-ended).
current_period_start / current_period_endTIMESTAMPThe billing window — drives renewal.
trial_started_at / trial_ends_at / trial_converted_at / trial_expired_atTIMESTAMPTrial lifecycle.
cancelled_at / cancellation_effective_at / cancellation_reasonTIMESTAMP / VARCHARCancellation.
auto_renewBOOL
activated_atTIMESTAMPFirst pending → active.
dunning_attempts / last_dunning_at / suspended_atINT / TIMESTAMPDunning bookkeeping.
last_event_seqBIGINTCursor for the event store.
metadataJSON
created_at / updated_at / deleted_atTIMESTAMPSoft-deletes enabled.

Key indexes drive the scheduler and lookups: (subscriber_type, subscriber_id, status), (status, current_period_end), (status, ends_at), (status, trial_ends_at), and (pending_change_at).

tashil_subscription_features (immutable snapshot)

A frozen view of a feature on a subscription, written on subscribe and on every plan change. Old rows are stamped superseded_at and never touched again.

ColumnTypeNotes
idBIGINT PK
subscription_id / feature_idBIGINT FKCascade on delete.
feature_slug / feature_typeVARCHARDenormalized so catalog renames don't affect history.
valueVARCHARThe pivot value at subscribe time.
reset_periodVARCHARSnapshotted.
added_at / superseded_atTIMESTAMPsuperseded_at is null for current rows.

Enforced immutability

SubscriptionFeature::booted() throws on any update to a column other than superseded_at / updated_at, and on any delete. The snapshot is a trustworthy audit trail by construction.

tashil_feature_usages (mutable counter)

The current usage value per (subscription, feature). Every change is also written to the usage log.

ColumnTypeNotes
idBIGINT PK
subscription_id / feature_idBIGINT FKCascade. Unique together.
usageDECIMAL(20,4)Current counter; fractional values supported.
limit_valueDECIMAL(20,4)Cached cap (null = unlimited). Populated only for limit features.
reset_periodVARCHARUsed by the reset job.
period_start / period_endTIMESTAMPThe reset job zeroes counters whose period_end has passed, anchored to the previous period_end.

tashil_usage_logs (append-only audit)

One row per mutation of a counter. Never updated or deleted in the public API.

ColumnTypeNotes
idBIGINT PK
subscription_id / feature_idBIGINT FKCascade.
operationVARCHARUsageOperation enum: consume, reset, adjust, report.
amountDECIMAL(20,4)
previous_usage / new_usageDECIMAL(20,4)Before/after snapshot — enables full replay.
description / metadataVARCHAR / JSON

tashil_subscription_events (immutable event store)

The append-only log of every transition. The Eloquent layer rejects updates and deletes.

ColumnTypeNotes
idBIGINT PK
event_idUUID UNIQUEStable external identifier.
subscription_idBIGINT FKCascade.
event_typeVARCHAR(64)e.g. subscription.created, trial.expired, usage.reset.
sequence_numBIGINTStrictly monotonic per subscription (assigned under a FOR UPDATE lock).
payload / metadataJSONDomain data / caller context.
idempotency_keyUUIDWhen set, a repeated append returns the existing row.
occurred_at / recorded_atTIMESTAMPLogical vs storage time.

Unique: (subscription_id, sequence_num) and (subscription_id, idempotency_key).

tashil_invoices

Bills Tashil issues — initial when a priced plan is subscribed (or a trial converts), renewal at period end, and proration on an in-place upgrade.

ColumnTypeNotes
idBIGINT PK
subscription_idBIGINT FKCascade.
invoice_numberVARCHAR UNIQUEGenerated by the observer via tashil.invoice.generator.
kindVARCHARInvoiceKind enum: initial, renewal, proration, usage — drives routing on payment.
amount / currencyDECIMAL(10,2) / CHAR(3)
statusVARCHARInvoiceStatus: draft, pending, paid, void, refunded.
period_start / period_endTIMESTAMPThe window the invoice covers.
issued_at / due_date / paid_atTIMESTAMP
attempts / last_attempt_atINT / TIMESTAMPDunning bookkeeping.
notesTEXT
created_at / updated_at / deleted_atTIMESTAMPSoft-deletes enabled.

When a paid invoice is observed, it routes by kind + subscription status:

  • initial + Pendingactivate() (first access, period anchored to paid_at).
  • renewal + Active/OnTrialadvancePeriod(), fires SubscriptionRenewed.
  • any kind + lapsed (PastDue/Suspended/Expired) → reactivate().
  • proration (or initial on an active sub) → records payment, no period change.

tashil_transactions

An optional ledger of gateway settlements linked to invoices. Tashil never writes here — your app records gateway responses.

ColumnTypeNotes
idBIGINT PK
invoice_idBIGINT FKCascade.
gatewayVARCHARDefaults to manual (e.g. stripe, paddle, bkash).
transaction_idVARCHARGateway-supplied, or auto-generated when blank (unique within its gateway).
amount / currency / status
gateway_response / metadataJSON
refunded_amount / refunded_at / refund_reason

Index: UNIQUE(gateway, transaction_id) — a reconciliation guard that makes duplicate webhook deliveries safe. transaction_id stays nullable so pre-gateway-response rows aren't blocked.

Soft deletes

packages, features, subscriptions, and invoices use Laravel soft-deletes. The other tables (snapshot, counter, logs, events, transactions) intentionally don't — they're either append-only audit or short-lived counters managed by their parent's cascade.

Connection & table-name overrides

Every table name and the connection come from config('tashil.database.*') at runtime. BaseModel resolves the table name from class_basename → snake_plural, looks up tashil.database.tables.<key>, and prefixes with tashil.database.prefix — so renaming a table or moving Tashil to its own database connection is pure configuration. See Configuration.

Tashil — Subscription management for Laravel. Released under the MIT license.